简体   繁体   中英

Generate prime number sequences, Loop Break vs LINQ TakeWhile

I try to generate prime number sequences with C#. The code seems to be working fine so far.

List<int> _primes = new List<int>();

bool IsPrime(int num)
{
    if (num < 2)
        return false;
    int chkEnd = (int)Math.Sqrt(num);

    foreach (var p in _primes)
    {
        if (p > chkEnd)
            break;
        if (num % p == 0)
            return false;
    }
    return true;
}

for (int i=2; i<1000000; i++)
{
    if (IsPrime(i))
        _primes.Add(i);
}

Then I change IsPrime() to use LINQ TakeWhile(), I think the two methods would have the same time complexity, but the execution time is significantly higher than the former.

bool IsPrime(int num)
{
    if (num < 2)
        return false;
    int chkEnd = (int)Math.Sqrt(num);

    foreach (var p in _primes.TakeWhile(x => x <= chkEnd))
    {
        if (num % p == 0)
            return false;
    }
    return true;
}

or

bool IsPrime(int num)
{
    if (num < 2)
        return false;
    int chkEnd = (int)Math.Sqrt(num);

    return _primes.TakeWhile(x => x <= chkEnd).All(p => num % p != 0);
}

Does anyone has any idea?


Edit: I increased the loop iteration to 5000000, run the code in a consoleApplication and output the execution time

00:00:01.1841593 -- IsPrime_PureLoopBreak

00:00:03.2560654 -- IsPrime_TakeWhileAndLoop

00:00:03.4178782 -- IsPrime_TakeWhileAndAll

    static void Main(string[] args)
    {
        List<int> _primes;

        bool IsPrime_PureLoopBreak(int num)
        {
            if (num < 2)
                return false;
            int chkEnd = (int)Math.Sqrt(num);

            foreach (var p in _primes)
            {
                if (p > chkEnd)
                    break;
                if (num % p == 0)
                    return false;
            }
            return true;
        }

        bool IsPrime_TakeWhileAndLoop(int num)
        {
            if (num < 2)
                return false;
            int chkEnd = (int)Math.Sqrt(num);

            foreach (var p in _primes.TakeWhile(x => x <= chkEnd))
            {
                if (num % p == 0)
                    return false;
            }
            return true;
        }

        bool IsPrime_TakeWhileAndAll(int num)
        {
            if (num < 2)
                return false;
            int chkEnd = (int)Math.Sqrt(num);

            return _primes.TakeWhile(x => x <= chkEnd).All(p => num % p != 0);
        }

        var t1 = Measure(() => 
        {
            _primes = new List<int>();
            for (int i = 2; i < 5000000; i++)
            {
                if (IsPrime_PureLoopBreak(i))
                    _primes.Add(i);
            }
        });
        Console.WriteLine($"{t1} -- IsPrime_PureLoopBreak");

        var t2 = Measure(() =>
        {
            _primes = new List<int>();
            for (int i = 2; i < 5000000; i++)
            {
                if (IsPrime_TakeWhileAndLoop(i))
                    _primes.Add(i);
            }
        });
        Console.WriteLine($"{t2} -- IsPrime_TakeWhileAndLoop");

        var t3 = Measure(() =>
        {
            _primes = new List<int>();
            for (int i = 2; i < 5000000; i++)
            {
                if (IsPrime_TakeWhileAndAll(i))
                    _primes.Add(i);
            }
        });
        Console.WriteLine($"{t3} -- IsPrime_TakeWhileAndAll");

        Console.ReadLine();
    }

    public static TimeSpan Measure(Action action)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        action?.Invoke();
        stopwatch.Stop();
        return stopwatch.Elapsed;
    }

Consider the code the compiler would need to generate. "High level" code like Linq is simple and easy to write, but is typically more difficult for the compiler to optimize well.

Your former example could be further simplified to use a plain for loop rather than a foreach. So each iteration should only need a handful of instructions.

when using linq the compiler will generate a proxy object for the iterator. This will need to call your lambda in order to check if it need to continue iteration, and the compiler might not be able to inline the method calls. Method calls are cheap, but still several times more expensive than simple arithmetic instructions.

So as a rule of thumb, use Linq and other high level patterns to improve readability. If you discover that some part does not of the code do not fulfill the performance goals, use a profiler to identify exactly what part takes time, and rewrite that part until it is fast enough or you have made it as fast as you can.

Using LINQ has some additional overhead. You are creating and calling lambda expressions.

But, make sure to try in a Release build before drawing conclusions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM