【问题标题】:Why is Enumerable.Range faster than a direct yield loop?为什么 Enumerable.Range 比直接 yield 循环快?
【发布时间】:2010-09-29 08:05:13
【问题描述】:

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

    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 方法有更多的开销。

这可能是什么原因?

【问题讨论】:

    标签: c# performance ienumerable range enumerable


    【解决方案1】:

    假设这是一个正在运行的发布版本,否则所有比较都将关闭,因为 JIT 将无法正常工作。

    您可以查看带有reflector 的程序集,并查看“yield”语句正在扩展什么。编译器将创建一个类来封装迭代器。生成的代码可能比 Enumerable.Range 的实现更多,而后者可能是手动编码的

    【讨论】:

      【解决方案2】:

      为什么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;
              }
          }
      }
      

      那么它应该和你自制的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; } }
          }
      }
      

      【讨论】:

        【解决方案3】:

        我的猜测是您正在调试器中运行。这是我的结果,使用“/o+ /debug-”从命令行构建

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

        仍有细微差别,但并不那么明显。迭代器块的实现不如量身定制的解决方案那么高效,但它们非常好。

        【讨论】:

        • 是的,我在调试模式下进行了实验。所以,自制的方法生成调试代码。发布速度要快得多。
        • 这里有两件事:在调试模式下构建,在调试器中运行而不是在没有附加调试器的情况下执行。后者有更大的不同。
        【解决方案4】:

        Reflector 输出略有不同(以及参数检查和额外的内部化水平在这里绝对不相关)。基本代码更像:

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

        也就是说,它们不是另一个局部变量,而是为每个产量应用一个额外的加法。

        我已尝试对此进行基准测试,但我无法停止足够多的外部流程来获得可理解的结果。我还尝试了每个测试两次以忽略 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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-10-01
          • 1970-01-01
          • 2023-04-09
          • 1970-01-01
          • 1970-01-01
          • 2020-06-26
          • 2016-05-07
          • 2019-07-17
          相关资源
          最近更新 更多