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

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

public static void Main(string[] args) { // for loop { Stopwatch sw = Stopwatch.StartNew(); int accumulator = 0; for (int i = 1; i  accumulator + n); sw.Stop(); Console.WriteLine("time = {0}; result = {1}", sw.ElapsedMilliseconds, ret); } //self-made IEnumerable { 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 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 

因为Enumerable.Aggregate需要更多的方法调用,所以“for循环”比其他两个解决方案更快也就不足为奇了。 然而,“Enumerable.Range”比“自制IEnumerable”更快,这让我感到惊讶。 我认为Enumerable.Range比简单的GetIntRange方法有更多的开销。

可能的原因是什么?

为什么Enumerable.Range要比你自制的GetIntRange ? 实际上,如果将Enumerable.Range定义为

 public static class Enumerable { public static IEnumerable 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 Range(int start, int count) { return new RangeEnumerable(start, count); } private class RangeEnumerable : IEnumerable { private int _Start; private int _Count; public RangeEnumerable(int start, int count) { _Start = start; _Count = count; } public virtual IEnumerator GetEnumerator() { return new RangeEnumerator(_Start, _Count); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } private class RangeEnumerator : IEnumerator { 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; } } } } 

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

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

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

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

您可以使用reflection器查看assembly,并查看“yield”语句的扩展情况。 编译器将创建一个类来封装迭代器。 也许生成的代码中的内务处理比Enumerable.Range的实现更多,这可能是手工编码的

reflection器输出的细微差别(以及参数检查和内部化的额外水平在这里绝对不相关)。 基本代码更像是:

 public static IEnumerable 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 { 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 { 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 GetIntRange(int start, int count) { int end = start + count; for (int i = start; i < end; ++i) { yield return i; } } private static IEnumerable GetRange(int start, int count) { for (int i = 0; i < count; ++i) { yield return start + i; } } } } 

用。编译

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