我和我的同事有一点争议(这与圣战非常接近:),关于通过枚举 VS 访问列表的性能。为了操作一些事实,我写了以下测试:
static void Main(string[] args)
{
const int count = 10000000;
var stopwatch = new Stopwatch();
var list = new List<int>(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<int> 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
:第一次测试中的列表和第二次测试中的一般列表。
答案 0 :(得分:17)
使用List<T>
时,foreach
实际上并未使用IEnumerable<T>
界面;相反,它使用List<T>.Enumerator
,即struct
。在平凡的层面上,这意味着稍微减少间接 - 不必去引用,使用静态调用而不是虚拟调用 - 以及更直接的实现。
这些差异非常小,在任何明智的现实生活中,差异都是噪音。但是,如果测试 foreach
性能,则可能会略显明显。
要对此进行扩展:foreach
实际上并不需要IEnumerable[<T>]
- 它可以在GetEnumerator()
/ .MoveNext()
上纯粹 / .Current
/ .Dispose()
模式;这在2.0中的泛型之前尤其重要。
但是,只有在将变量键入List<T>
(具有自定义GetEnumerator()
方法)时才可以执行此操作。获得IEnumerable<T>
后,必须使用IEnumerator<T>
答案 1 :(得分:4)
您可以在此处查看代码:
static void Main()
{
List<int> list = new List<int>(Enumerable.Range(1,10000));
int total = 0;
foreach (var i in list)
{
total += i;
}
IEnumerable<int> enumerable = list;
foreach (var i in enumerable)
{
total += i;
}
Console.ReadLine();
}
哪个产生这个IL。注意
之间的区别System.Collections.Generic.List`1/Enumerator<int32>
和
System.Collections.Generic.IEnumerable`1<int32>
并注意它是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<int32> list,
[1] int32 total,
[2] int32 i,
[3] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> enumerable,
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
[5] bool CS$4$0001,
[6] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> 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<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_000c: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<!0>)
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<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::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<int32>::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<int32>::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<int32>
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<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::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<int32>::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
答案 2 :(得分:2)
如果您查看两个版本的IL,您将看到第一个版本使用类型为System.Collections.Generic.List<System.Int32>+Enumerator
的迭代器 - 嵌套的struct
,它针对迭代列表进行了优化。 / p>
第二个版本使用System.Collections.Generic.IEnumerator<System.Int32>
的通用实现,效率较低,因为它不会通过保留列表中当前项的私有索引来“欺骗”。
答案 3 :(得分:1)
我怀疑使用forach而不是foreach(至少对于原始类型)有性能提升。据我所知,如果你在同一个数组上执行和foreach(而不是像列表这样的任何其他结构,它们本身就会产生一些开销)。
foreach和for的性能取决于您运行的结构类型和foreach。