Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

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进行比较,测试框架如下:

Code

 

首先来比较基于数组的性能:

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代码:

  1. IL_005c:  ldc.i4.0      
  2. IL_005d:  stloc.s    V_7           //初始化循环变量
  3. IL_005f:  br.s       IL_006d      //分支跳转
  4. IL_0061:  ldloc.2                     //把数组压入evaluation stack
  5. IL_0062:  ldloc.s    V_7           //当前索引
  6. IL_0064:  ldelem.i4                 //取得当前元素
  7. IL_0065:  stloc.s    result        //放入result
  8. IL_0067:  ldloc.s    V_7
  9. IL_0069:  ldc.i4.1
  10. IL_006a:  add                         //更新索引
  11. IL_006b:  stloc.s    V_7
  12. IL_006d:  ldloc.s    V_7
  13. IL_006f:  ldloc.2
  14. IL_0070:  ldlen                       //把数组元素个数压入evaluation stack
  15. IL_0071:  conv.i4
  16. IL_0072:  blt.s      IL_0061    //测试循环是否继续

foreach版本:

  1. IL_005f:  ldc.i4.0
  2. L_0060:  stloc.s    CS$7$0001
  3. L_0062:  br.s       IL_0075
  4. L_0064:  ldloc.s    CS$6$0000
  5. L_0066:  ldloc.s    CS$7$0001
  6. L_0068:  ldelem.i4
  7. L_0069:  stloc.s    V_7
  8. L_006b:  ldloc.s    V_7
  9. L_006d:  stloc.s    result
  10. L_006f:  ldloc.s    CS$7$0001
  11. L_0071:  ldc.i4.1
  12. L_0072:  add
  13. L_0073:  stloc.s    CS$7$0001
  14. L_0075:  ldloc.s    CS$7$0001
  15. L_0077:  ldloc.s    CS$6$0000
  16. L_0079:  ldlen
  17. L_007a:  conv.i4
  18. 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这样的高级指令:

  1. .try
  2. {
  3.   IL_0064:  br.s       IL_0073
  4.   IL_0066:  ldloca.s   CS$5$0000
  5.   IL_0068:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
  6.   IL_006d:  stloc.s    V_7
  7.   IL_006f:  ldloc.s    V_7
  8.   IL_0071:  stloc.s    result
  9.   IL_0073:  ldloca.s   CS$5$0000
  10.   IL_0075:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
  11.   IL_007a:  brtrue.s   IL_0066
  12.   IL_007c:  leave.s    IL_008c
  13. }  // end .try
  14. finally
  15. {
  16.   IL_007e:  ldloca.s   CS$5$0000
  17.   IL_0080:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
  18.   IL_0086:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
  19.   IL_008b:  endfinally
  20. }  // end handler

        对list来说,foreach是通过Enumerator而不是基于简单的索引进行迭代。好了,你可能会说一开始不是说过List不会分配Enumerator吗?确实,这里虽然用到了Enumerator,但是并没有分配内存创建Enumerator。如果说ms对foreach进行了优化,那么应该就是值的这里吧,注意,这里仍然把结果进行了一次额外赋值。
       for版本:

  1. IL_005c:  ldc.i4.0
  2. IL_005d:  stloc.s    V_7
  3. IL_005f:  br.s       IL_0071
  4. IL_0061:  ldloc.1
  5. IL_0062:  ldloc.s    V_7
  6. IL_0064:  callvirt   instance !0 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Item(int32)
  7. IL_0069:  stloc.s    result
  8. IL_006b:  ldloc.s    V_7
  9. IL_006d:  ldc.i4.1
  10. IL_006e:  add
  11. IL_006f:  stloc.s    V_7
  12. IL_0071:  ldloc.s    V_7
  13. IL_0073:  ldloc.1
  14. IL_0074:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
  15. 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倍左右:)

 

posted on 2009-05-17 23:18  clayman  阅读(1065)  评论(5编辑  收藏  举报