zoukankan      html  css  js  c++  java
  • 0.033秒的艺术 for vs. foreach

    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倍左右:)

  • 相关阅读:
    P1378 油滴扩展
    P1219 [USACO1.5]八皇后 Checker Challenge
    P1126 机器人搬重物
    Mac鼠标和触控板完美使用
    B词
    一个开发狗的时间线
    快速排序
    TikTok直播研发校招专属内推
    Jupyter Lab + anaconda 环境搭建
    React环境搭建
  • 原文地址:https://www.cnblogs.com/clayman/p/1459029.html
Copyright © 2011-2022 走看看