Home > Enterprise >  Sieve of Eratosthenes run slower after improving
Sieve of Eratosthenes run slower after improving

Time:02-18

I try to improve basic Sieve of Eratosthenes algorithm by avoiding cross out duplicate multiple of primes, but it turn out to be worse than my expectation

I has implemented two method that return primes in range [2..max)

Basic sieve

public static List<int> Sieve22Max_Basic(int n) {
    var primes = new List<int>();
    var sieve = new BitArray(n, true); // default all number are prime
    //int crossTotal = 0;

    int sqrt_n = (int)Math.Sqrt(n);
    for (int p = 2; p < sqrt_n;   p) {
        if (sieve[p]) {
            primes.Add(p);
            //var cross = new List<int>();
            int inc = p == 2 ? p : 2 * p;
            for (int mul = p * p; mul < n; mul  = inc) {
                // cross out multiple of prime p
                // cross.Add(mul);
                //  crossTotal;
                sieve[mul] = false;
            }

            //if (cross.Count > 0)
            //    Console.WriteLine($"Prime {p}, cross out: {string.Join(' ', cross)}");
        }
    }
    //Console.WriteLine($"crossTotal: {crossTotal:n0}");

    for (int p = sqrt_n; p < n;   p)
        if (sieve[p])
            primes.Add(p);

    return primes;
}

Run Sieve22Max_Basic(100), see some multiples are cross more than one (ex: 45, 75, 63)

Prime 2, cross out: 4 6 8 ... 96 98
Prime 3, cross out: 9 15 21 27 33 39 45 51 57 63 69 75 81 87 93 99
Prime 5, cross out: 25 35 45 55 65 75 85 95
Prime 7, cross out: 49 63 77 91

Enhance Sieve

Then, I try to improve by using array that store smallest prime divisor (spd) of each number.

45 = 3 x 5      // spd[45] = 3
75 = 3 x 5 x 5  // spd[75] = 3
63 = 3 x 3 x 7  // spd[63] = 3

When go through multiple of prime p, I will not cross out number mul that have spd[mul] < p because mul was cross out by spd[mul] before

public static List<int> Sieve22Max_Enh(int n) {
    var sieve = new BitArray(n, true);
    var spd = new int[n];
    for (int i = 0; i < n;   i) spd[i] = i;
    
    var primes = new List<int>();
    //int crossTotal = 0;

    int sqrt_n = (int)Math.Sqrt(n);
    for (int p = 2; p < sqrt_n;   p) {
        if (sieve[p]) {
            primes.Add(p);

            //var cross = new List<int>();
            int inc = p == 2 ? 1 : 2;
            for (long mul = p; mul * p < n; mul  = inc) {
                if (spd[mul] >= p) {
                    sieve[(int)(mul * p)] = false;
                    spd[mul * p] = p;
                    //  crossTotal;
                    //cross.Add((int)(mul * p));
                }
            }
            //if (cross.Count > 0)
            //    Console.WriteLine($"Prime {p}, cross out: {string.Join(' ', cross)}");
        }
    }
    //Console.WriteLine($"crossTotal: {crossTotal:n0}");

    for (int p = sqrt_n; p < n;   p)
        if (sieve[p])
            primes.Add(p);

    return primes;
}

Test

I test on my laptop (core i7 - 2.6 Ghz), with n = 1 billion

Sieve22Max_Basic take only 6s while Sieve22Max_Enh take more than 10s to complete

var timer = new Stopwatch();
int n = 1_000_000_000;

timer.Restart();
Console.WriteLine("==== Sieve22Max_Basic ===");
var list = Sieve22Max_Basic(n);
Console.WriteLine($"Count: {list.Count:n0}, Last: {list[list.Count - 1]:n0}, elapsed: {timer.Elapsed}");

Console.WriteLine();

timer.Restart();
Console.WriteLine("==== Sieve22Max_Enh ===");
list = Sieve22Max_Enh(n);
Console.WriteLine($"Count: {list.Count:n0}, Last: {list[list.Count - 1]:n0}, elapsed: {timer.Elapsed}");

You can try at https://onlinegdb.com/tWfMuDDK0

What does it make slower?

CodePudding user response:

Compare your two loops from the original and improved versions.

Original:

int inc = p == 2 ? p : 2 * p;
for (int mul = p * p; mul < n; mul  = inc) {
  sieve[mul] = false;
}

Improved:

int inc = p == 2 ? 1 : 2;
for (long mul = p; mul * p < n; mul  = inc) {
  if (spd[mul] >= p) {
    sieve[(int)(mul * p)] = false;
    spd[mul * p] = p;
  }
}

Some observations:

  • Both loops run the same amount of iterations.
  • For every iteration, the original loop executes three very fast operations: 1) change a value in BitArray, mul = inc and check mul < n.
  • For every iteration of the improved loop, we execute more operations: check spd[mul] >= p, mul = inc, mul * p (in the for-loop condition), check mul * p < n.
  • Increment = and loop condition check for < are the same in both loops; check spd[mul] >= p and change a value in BitArray are comparable in how long they take; but the additional op mul * p in the second loop's condition is multiplication – it's expensive!
  • But also, for every iteration of the second loop, if spd[mul] >= p is true, then we also execute: mul * p (again!), cast to int, change a value in BitArray, mul * p(third time!), I assume again a cast to int in the indexing of spd, and assign a value in the array spd.

To summarize, your second improved loop's every iteration is computationally "heavier". This is the reason why your improved version is slower.

  • Related