通过接口枚举 – 性能损失

我与我的同事有一点争议(这非常接近圣战:))关于通过枚举VS通过枚举器访问列表的性能。 为了操作一些事实,我写了以下测试:

static void Main(string[] args) { const int count = 10000000; var stopwatch = new Stopwatch(); var list = new List(count); var rnd = new Random(); for (int i = 0; i < count; i++) { list.Add( rnd.Next()); } const int repeat = 20; double indeces = 0; double forEach = 0; for (int iteration = 0; iteration < repeat; iteration++) { stopwatch.Restart(); long tmp = 0; for (int i = 0; i < count; i++) { tmp += list[i]; } indeces += stopwatch.Elapsed.TotalSeconds; stopwatch.Restart(); foreach (var integer in list) { tmp += integer; } forEach += stopwatch.Elapsed.TotalSeconds; } Console.WriteLine(indeces /repeat); Console.WriteLine(forEach /repeat); } 

实际上,它只是访问元素。

正如我所料,索引访问速度更快。 这是我的机器上发布版本的结果:

  0.0347//index access 0.0737//enumerating 

但是,我决定改变测试一点:

  //the same as before ... IEnumerable listAsEnumerable = list; //the same as before ... foreach (var integer in listAsEnumerable) { tmp += integer; } ... 

现在输出如下:

  0.0321//index access 0.1246//enumerating (2x slower!) 

如果我们通过接口枚举相同的列表,性能会慢2倍

为什么会这样?

意味着“通过接口枚举比枚举实际列表慢2倍”。

我的猜测是运行时使用不同的Enumerator :第一次测试中的列表和第二次测试中的一般列表。

使用Listforeach实际上并不使用IEnumerable接口; 相反,它使用List.Enumerator ,它是一个struct 。 在平凡的层面上,这意味着稍微减少间接 – 不必去引用,使用静态调用而不是虚拟调用 – 以及更直接的实现。

这些差异非常小,在任何明智的现实生活中,差异都是噪音。 但是,如果测试foreach性能,则可能会略微明显。

为了扩展这个: foreach实际上并不需要IEnumerable[] – 它可以完全依赖于GetEnumerator() / .MoveNext() / .Current .Dispose() / .Dispose()模式; 这在2.0中的generics之前尤为重要。

但是,只有在将变量键入List (具有自定义GetEnumerator()方法)时才可以执行此操作。 一旦你有了IEnumerable ,就必须使用IEnumerator

你可以在这里看到代码:

 static void Main() { List list = new List(Enumerable.Range(1,10000)); int total = 0; foreach (var i in list) { total += i; } IEnumerable enumerable = list; foreach (var i in enumerable) { total += i; } Console.ReadLine(); } 

这产生了这个IL。 注意之间的区别

 System.Collections.Generic.List`1/Enumerator 

 System.Collections.Generic.IEnumerable`1 

并注意它是一个ValueType (struct):

 .method private hidebysig static void Main() cil managed { .entrypoint // Code size 146 (0x92) .maxstack 2 .locals init ([0] class [mscorlib]System.Collections.Generic.List`1 list, [1] int32 total, [2] int32 i, [3] class [mscorlib]System.Collections.Generic.IEnumerable`1 enumerable, [4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator CS$5$0000, [5] bool CS$4$0001, [6] class [mscorlib]System.Collections.Generic.IEnumerator`1 CS$5$0002) IL_0000: nop IL_0001: ldc.i4.1 IL_0002: ldc.i4 0x2710 IL_0007: call class [mscorlib]System.Collections.Generic.IEnumerable`1 [System.Core]System.Linq.Enumerable::Range(int32, int32) IL_000c: newobj instance void class [mscorlib]System.Collections.Generic.List`1::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1) IL_0011: stloc.0 IL_0012: ldc.i4.0 IL_0013: stloc.1 IL_0014: nop IL_0015: ldloc.0 IL_0016: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator class [mscorlib]System.Collections.Generic.List`1::GetEnumerator() IL_001b: stloc.s CS$5$0000 .try { IL_001d: br.s IL_002d IL_001f: ldloca.s CS$5$0000 IL_0021: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::get_Current() IL_0026: stloc.2 IL_0027: nop IL_0028: ldloc.1 IL_0029: ldloc.2 IL_002a: add IL_002b: stloc.1 IL_002c: nop IL_002d: ldloca.s CS$5$0000 IL_002f: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::MoveNext() IL_0034: stloc.s CS$4$0001 IL_0036: ldloc.s CS$4$0001 IL_0038: brtrue.s IL_001f IL_003a: leave.s IL_004b } // end .try finally { IL_003c: ldloca.s CS$5$0000 IL_003e: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator IL_0044: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0049: nop IL_004a: endfinally } // end handler IL_004b: nop IL_004c: ldloc.0 IL_004d: stloc.3 IL_004e: nop IL_004f: ldloc.3 IL_0050: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1 class [mscorlib]System.Collections.Generic.IEnumerable`1::GetEnumerator() IL_0055: stloc.s CS$5$0002 .try { IL_0057: br.s IL_0067 IL_0059: ldloc.s CS$5$0002 IL_005b: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1::get_Current() IL_0060: stloc.2 IL_0061: nop IL_0062: ldloc.1 IL_0063: ldloc.2 IL_0064: add IL_0065: stloc.1 IL_0066: nop IL_0067: ldloc.s CS$5$0002 IL_0069: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_006e: stloc.s CS$4$0001 IL_0070: ldloc.s CS$4$0001 IL_0072: brtrue.s IL_0059 IL_0074: leave.s IL_008a } // end .try finally { IL_0076: ldloc.s CS$5$0002 IL_0078: ldnull IL_0079: ceq IL_007b: stloc.s CS$4$0001 IL_007d: ldloc.s CS$4$0001 IL_007f: brtrue.s IL_0089 IL_0081: ldloc.s CS$5$0002 IL_0083: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0088: nop IL_0089: endfinally } // end handler IL_008a: nop IL_008b: call string [mscorlib]System.Console::ReadLine() IL_0090: pop IL_0091: ret } // end of method Program2::Main 

如果您查看两个版本的IL,您将看到第一个版本使用System.Collections.Generic.List+Enumerator类型的迭代器System.Collections.Generic.List+Enumerator – 一个嵌套的struct ,它针对迭代列表进行了优化。

第二个版本使用System.Collections.Generic.IEnumerator的通用实现,效率较低,因为它不会通过保留列表中当前项的私有索引来“欺骗”。

我怀疑使用forach而不是foreach(至少对于原始类型)有性能提升。 据我所知,如果你在同一个数组上执行和foreach(不是像列表这样的任何其他结构,它们本身会产生一些开销),它们几乎是等价的。

foreach和for的性能取决于您运行的结构类型和foreach。

请检查; 用于和Foreach比较