0.033秒的艺术 --- for vs. foreach
仅供个人学习使用,请勿转载,勿用于任何商业用途。
一直以来,关于C#中for和foreach孰优孰劣的争论似乎从来就没有停止过,各种谣言和真相混杂在一起,以至于这似乎变成了一个很复杂的问题。For的fans认为,foreach内部会分配enumerator,产生垃圾,影响性能;而foreach的支持者则认为编译器会对foreach进行特别的优化,而且也更安全(Efective C#里就是这么写的)。那么到底是不是这样呢?
实际上,关于foreach会分配enumerator是一个最大的谣言!Cornflower Blue通过实验得出的结论是:
1. Collection<T>会分配enumerator
2. System.Collections.Generic空间下的大部分类型则不会分配enumerator,比如list,queues,linked lists等等
3. 如果把2中的类型转换为接口使用foreach,则会分配enumerator,比如 foreach item in (IEnumerable)List
如果你对此有所怀疑,可以用CLRProfiler做个简单测试看看,foreach并没有我们想象的那么坏.
除了内存占用的谣言之外,性能又如何呢?哪个比较快。让我们开始测试吧。当然,首先应该清楚不同类型来说,for和foreach的性能自然也不同。为了方便讨论,下文仅使用用array和list进行比较,测试框架如下:
首先来比较基于数组的性能:
test case 1:
for(int i = 0;i<arr.lenght;i++)
result = arr[i];
test case 2:
foreach (int i in arr)
result = i;
测试结果,两者几乎相同,for的版本稍稍比foreach快一点点。Foreach的死党看到这里也许会有些不服气,认为这是测试误差的结果,不过接下来就来分析一下为什么会出现这样的结果,用ildasm反编译代码可以看到对于for的版本产生了如下IL代码:
- IL_005c: ldc.i4.0
- IL_005d: stloc.s V_7 //初始化循环变量
- IL_005f: br.s IL_006d //分支跳转
- IL_0061: ldloc.2 //把数组压入evaluation stack
- IL_0062: ldloc.s V_7 //当前索引
- IL_0064: ldelem.i4 //取得当前元素
- IL_0065: stloc.s result //放入result
- IL_0067: ldloc.s V_7
- IL_0069: ldc.i4.1
- IL_006a: add //更新索引
- IL_006b: stloc.s V_7
- IL_006d: ldloc.s V_7
- IL_006f: ldloc.2
- IL_0070: ldlen //把数组元素个数压入evaluation stack
- IL_0071: conv.i4
- IL_0072: blt.s IL_0061 //测试循环是否继续
foreach版本:
- IL_005f: ldc.i4.0
- L_0060: stloc.s CS$7$0001
- L_0062: br.s IL_0075
- L_0064: ldloc.s CS$6$0000
- L_0066: ldloc.s CS$7$0001
- L_0068: ldelem.i4
- L_0069: stloc.s V_7
- L_006b: ldloc.s V_7
- L_006d: stloc.s result
- L_006f: ldloc.s CS$7$0001
- L_0071: ldc.i4.1
- L_0072: add
- L_0073: stloc.s CS$7$0001
- L_0075: ldloc.s CS$7$0001
- L_0077: ldloc.s CS$6$0000
- L_0079: ldlen
- L_007a: conv.i4
- L_007b: blt.s IL_0064
可以看到,两者的代码是非常相似的,又一个谣言不攻自破了,编译器并没有为foreach做特别的优化,foreach的版本反而比for多用了2条指令,一共18条指令。也正是这2条指令让foreach比for稍稍慢了一点。对于foreach这两条而外的指令我是觉得很奇怪的,代码先把从数组中取出的值复制到了临时变量V_7,然后再赋给了result,for则没有这个步骤。一开始我还以为是.net 2.0优化的不够好,换成3.5 sp1之后,还是相同的代码。我不是编译器也不是IL的专家,实在不明白为什么要这样做。需要注意,对于复杂的值类型,比如matrix,那么这两条额外的指令还会让情况变的更糟,因为值类型总是按值传递,每次对Matrix赋值,至少要对16个int进行操作。实际测试中,把int改为Matrix之后,foreach比for慢了3倍,这样的结果应该是出乎很多人意料的吧!当然,对于引用类型则没有这个问题。
接下来,我们对List进行测试:
test case 3:
for(int i = 0;i<list.count;i++)
result = list [i];
test case 4:
foreach (int i in list)
result = i;
与简单的数组相比,foreach为List生成的代码相当不同,甚至可以看到try和catch这样的高级指令:
- .try
- {
- IL_0064: br.s IL_0073
- IL_0066: ldloca.s CS$5$0000
- IL_0068: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
- IL_006d: stloc.s V_7
- IL_006f: ldloc.s V_7
- IL_0071: stloc.s result
- IL_0073: ldloca.s CS$5$0000
- IL_0075: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
- IL_007a: brtrue.s IL_0066
- IL_007c: leave.s IL_008c
- } // end .try
- finally
- {
- IL_007e: ldloca.s CS$5$0000
- IL_0080: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
- IL_0086: callvirt instance void [mscorlib]System.IDisposable::Dispose()
- IL_008b: endfinally
- } // end handler
对list来说,foreach是通过Enumerator而不是基于简单的索引进行迭代。好了,你可能会说一开始不是说过List不会分配Enumerator吗?确实,这里虽然用到了Enumerator,但是并没有分配内存创建Enumerator。如果说ms对foreach进行了优化,那么应该就是值的这里吧,注意,这里仍然把结果进行了一次额外赋值。
for版本:
- IL_005c: ldc.i4.0
- IL_005d: stloc.s V_7
- IL_005f: br.s IL_0071
- IL_0061: ldloc.1
- IL_0062: ldloc.s V_7
- IL_0064: callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Item(int32)
- IL_0069: stloc.s result
- IL_006b: ldloc.s V_7
- IL_006d: ldc.i4.1
- IL_006e: add
- IL_006f: stloc.s V_7
- IL_0071: ldloc.s V_7
- IL_0073: ldloc.1
- IL_0074: callvirt instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
- IL_0079: blt.s IL_0061
List的for版本则和array差不多。从代码IL就能看出,for版本显然要比foreach简单很多,当然foreach有更好的安全性。实际测试中,for大约比foreach快2倍。此外,注意到for版本的IL代码在每次迭代时都调用了一个GetCount()获取list的实际长度。与array.length不同,list.count对应着一个虚方法,所以在循环初始化时并不会被内联。如果你认为这样是多余的,那么可以把test 3改为:
当然,这也增加了你程序出错的可能性。
test case 5:
int count = list.count;
for(int i = 0;i<count;i++)
result = list [i];
最后,最终的测试结果还显示,对于for来说,基于数组的循环要比list快5倍左右:)