为什么Enumerable.Range比直接使用yield循环更快?

11

以下代码检查了三种不同的方法执行相同解决方案的性能。

    public static void Main(string[] args)
    {
        // for loop
        {
            Stopwatch sw = Stopwatch.StartNew();

            int accumulator = 0;
            for (int i = 1; i <= 100000000; ++i)
            {
                accumulator += i;
            }

            sw.Stop();

            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, accumulator);
        }

        //Enumerable.Range
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = Enumerable.Range(1, 100000000).Aggregate(0, (accumulator, n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }

        //self-made IEnumerable<int>
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = GetIntRange(1, 100000000).Aggregate(0, (accumulator, n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
    }

    private static IEnumerable<int> GetIntRange(int start, int count)
    {
        int end = start + count;

        for (int i = start; i < end; ++i)
        {
            yield return i;
        }
    }
}

结果如下:

time = 306; result = 987459712
time = 1301; result = 987459712
time = 2860; result = 987459712

“for循环”比其他两种解决方案更快并不奇怪,因为Enumerable.Aggregate需要更多的方法调用。但是,令我惊讶的是,“Enumerable.Range”比“自制IEnumerable”更快。我认为Enumerable.Range的开销会比简单的GetIntRange方法更大。

可能的原因是什么?

4个回答

12

为什么Enumerable.Range比你自己编写的GetIntRange慢呢?事实上,如果Enumerable.Range被定义为:

public static class Enumerable {
    public static IEnumerable<int> Range(int start, int count) {
        var end = start + count;
        for(var current = start; current < end; ++current) {
            yield return current;
        }
    }
}

那么,System.Linq.Enumerable.Range的速度应该与您自己制作的GetIntRange一样快。实际上,这是Enumerable.Range的参考实现,在没有编译器或程序员的任何技巧的情况下。

您可能想将您的 GetIntRangeSystem.Linq.Enumerable.Range 与以下实现进行比较(当然,记得在发布模式下编译,如 Rob 所指出的)。这个实现可能稍微优化了迭代块产生的编译器生成的内容。

public static class Enumerable {
    public static IEnumerable<int> Range(int start, int count) {
        return new RangeEnumerable(start, count);
    }
    private class RangeEnumerable : IEnumerable<int> {
        private int _Start;
        private int _Count;
        public RangeEnumerable(int start, int count) {
            _Start = start;
            _Count = count;
        }
        public virtual IEnumerator<int> GetEnumerator() {
            return new RangeEnumerator(_Start, _Count);
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }
    }
    private class RangeEnumerator : IEnumerator<int> {
        private int _Current;
        private int _End;
        public RangeEnumerator(int start, int count) {
            _Current = start - 1;
            _End = start + count;
        }
        public virtual void Dispose() {
            _Current = _End;
        }
        public virtual void Reset() {
            throw new NotImplementedException();
        }
        public virtual bool MoveNext() {
            ++_Current;
            return _Current < _End;
        }
        public virtual int Current { get { return _Current; } }
        object IEnumerator.Current { get { return Current; } }
    }
}

5

我的猜测是你正在运行调试器。以下是我的结果,使用命令行构建时加上"/o+ /debug-"。

time = 142; result = 987459712
time = 1590; result = 987459712
time = 1792; result = 987459712

虽然还有一些细微的差别,但不是很明显。迭代器块实现并不像定制解决方案那样高效,但它们相当不错。


是的,我在调试模式下进行了实验。因此,自制的方法会生成调试代码。发布版本则要快得多。 - Morgan Cheng
5
这里有两个要素:以调试模式构建和在调试器中运行,而不是在没有附加调试器的情况下执行。后者更为重要。 - Jon Skeet

4
假设这是一个发布版本的运行,否则所有比较都会有误,因为JIT不会全力工作。
你可以使用反编译工具Reflector查看程序集,并查看'yield'语句被展开成什么。编译器将创建一个类来封装迭代器。也许在生成的代码中有更多的清理工作,而Enumerable.Range的实现可能是手写的。

2

反射器输出略有不同(参数检查和额外的内部化水平在这里肯定不相关)。本质代码更像是:

public static IEnumerable<int> Range(int start, int count) {
    for(int current = 0; current < count; ++current) {
        yield return start + current;
    }
}

那就是说,他们不是将另一个本地变量应用于每个 yield,而是针对每个 yield 应用额外的加法运算。
我尝试过进行基准测试,但我无法停止足够多的外部进程以获得可理解的结果。我还尝试了两次测试来忽略 JIT 编译器的影响,但即使这样也有“有趣”的结果。
以下是我的一些结果示例:
第 0 次运行: 时间 = 4149;结果 = 405000000450000000 时间 = 25645;结果 = 405000000450000000 时间 = 39229;结果 = 405000000450000000 时间 = 29872;结果 = 405000000450000000
时间 = 4277;结果 = 405000000450000000 时间 = 26878;结果 = 405000000450000000 时间 = 26333;结果 = 405000000450000000 时间 = 26684;结果 = 405000000450000000
第 1 次运行: 时间 = 4063;结果 = 405000000450000000 时间 = 22714;结果 = 405000000450000000 时间 = 34744;结果 = 405000000450000000 时间 = 26954;结果 = 405000000450000000
时间 = 4033;结果 = 405000000450000000 时间 = 26657;结果 = 405000000450000000 时间 = 25855;结果 = 405000000450000000 时间 = 25031;结果 = 405000000450000000
第 2 次运行: 时间 = 4021;结果 = 405000000450000000 时间 = 21815;结果 = 405000000450000000 时间 = 34304;结果 = 405000000450000000 时间 = 32040;结果 = 405000000450000000
时间 = 3993;结果 = 405000000450000000 时间 = 24779;结果 = 405000000450000000 时间 = 29275;结果 = 405000000450000000 时间 = 32254;结果 = 405000000450000000
代码如下:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;

namespace RangeTests
{
  class TestRange
  {
    public static void Main(string[] args)
    {
      for(int l = 1; l <= 2; ++l)
      {
        const int N = 900000000;
        System.GC.Collect(2);
        // for loop
        {
            Stopwatch sw = Stopwatch.StartNew();

            long accumulator = 0;
            for (int i = 1; i <= N; ++i)
            {
                accumulator += i;
            }

            sw.Stop();

            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, accumulator);
        }
        System.GC.Collect(2);

        //Enumerable.Range
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = Enumerable.Range(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
        System.GC.Collect(2);

        //self-made IEnumerable<int>
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = GetIntRange(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
        System.GC.Collect(2);

        //self-made adjusted IEnumerable<int>
        {
            Stopwatch sw = Stopwatch.StartNew();

            var ret = GetRange(1, N).Aggregate(0, (long accumulator,int n) => accumulator + n);

            sw.Stop();
            Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret);
        }
        System.GC.Collect(2);
        Console.WriteLine();
    } }

    private static IEnumerable<int> GetIntRange(int start, int count)
    {
        int end = start + count;

        for (int i = start; i < end; ++i)
        {
            yield return i;
        }
    }

    private static IEnumerable<int> GetRange(int start, int count)
    {
        for (int i = 0; i < count; ++i)
        {
            yield return start + i;
        }
    }
} }

编译使用

csc.exe -optimize+ -debug- RangeTests.cs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接