代码之家  ›  专栏  ›  技术社区  ›  Morgan Cheng

为什么枚举。范围比直接产量循环快?

  •  11
  • Morgan Cheng  · 技术社区  · 16 年前

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

        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。聚合需要更多的方法调用。然而,令我惊讶的是,“Enumerable.Range”比“自制IEnumerable”更快。我原以为那是可枚举的。Range将比简单的GetIntRange方法有更多的开销。

    可能的原因是什么?

    4 回复  |  直到 14 年前
        1
  •  12
  •   yfeldblum    14 年前

    为什么要 Enumerable.Range 比你自制的慢一点 GetIntRange ?事实上,如果 可枚举。范围 被定义为

    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 。这实际上是 可枚举。范围 ,编译器或程序员没有任何技巧。

    你可能想比较一下你的 GetIntRange System.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; } }
        }
    }
    
        2
  •  5
  •   Jon Skeet    16 年前

    我猜你正在调试器中运行。以下是我使用“/o+/debug-”命令行构建的结果

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

    仍然有轻微的差异,但没有那么明显。迭代器块实现不如量身定制的解决方案高效,但它们相当不错。

        3
  •  4
  •   Rob Walker    16 年前

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

    你可以用 reflector 看看“收益率”声明也在扩展什么。编译器将创建一个类来封装迭代器。也许生成的代码中比Enumerable的实现有更多的内务处理。可能是手工编码的范围

        4
  •  2
  •   Mark Hurd    15 年前

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

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

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

    我试图对此进行基准测试,但我无法阻止足够的外部流程来获得可理解的结果。我还尝试了两次每个测试,以忽略JIT编译器的影响,但即使这样也有“有趣”的结果。

    以下是我的结果示例:

    Run 0:
    time = 4149; result = 405000000450000000
    time = 25645; result = 405000000450000000
    time = 39229; result = 405000000450000000
    time = 29872; result = 405000000450000000
    
    time = 4277; result = 405000000450000000
    time = 26878; result = 405000000450000000
    time = 26333; result = 405000000450000000
    time = 26684; result = 405000000450000000
    
    Run 1:
    time = 4063; result = 405000000450000000
    time = 22714; result = 405000000450000000
    time = 34744; result = 405000000450000000
    time = 26954; result = 405000000450000000
    
    time = 4033; result = 405000000450000000
    time = 26657; result = 405000000450000000
    time = 25855; result = 405000000450000000
    time = 25031; result = 405000000450000000
    
    Run 2:
    time = 4021; result = 405000000450000000
    time = 21815; result = 405000000450000000
    time = 34304; result = 405000000450000000
    time = 32040; result = 405000000450000000
    
    time = 3993; result = 405000000450000000
    time = 24779; result = 405000000450000000
    time = 29275; result = 405000000450000000
    time = 32254; result = 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