【问题标题】:Enumerating via interface - performance loss通过接口枚举 - 性能损失
【发布时间】:2012-05-17 11:15:57
【问题描述】:

我和我的同事有一点争议(这非常接近圣战:)),关于通过枚举器通过 indeces 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倍

为什么会发生这种情况?

this 表示“通过接口枚举比枚举实际列表慢 2 倍”。

我的猜测是运行时使用不同的Enumerators:列表在第一个测试中,而在第二个测试中是通用列表。

【问题讨论】:

    标签: c# performance list ienumerable


    【解决方案1】:

    当使用List&lt;T&gt;时,foreach实际上并没有使用IEnumerable&lt;T&gt;接口;相反,它使用List&lt;T&gt;.Enumerator,即struct。在微不足道的层面上,这意味着更少的间接性 - 不必取消引用,并且使用静态调用而不是虚拟调用 - 以及更直接的实现。

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

    对此进行扩展:foreach 实际上并不需要 IEnumerable[&lt;T&gt;] - 它可以纯粹GetEnumerator() / .MoveNext() / .Current / .Dispose() 模式上工作;这在 2.0 中的泛型之前尤其重要。

    但是,这只有在变量类型为 List&lt;T&gt;(具有自定义 GetEnumerator() 方法)时才有可能。一旦你有了IEnumerable&lt;T&gt;,它就必须使用IEnumerator&lt;T&gt;

    【讨论】:

    • 很好的答案!我实际上查看了两个版本的反汇编代码,并注意到使用了不同的枚举器。但是我对'il'的了解很差,所以我决定在这里问:)
    【解决方案2】:

    您可以在此处查看代码:

    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(结构):

    .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
    

    【讨论】:

      【解决方案3】:

      如果您查看两个版本的 IL,您会发现第一个版本使用了 System.Collections.Generic.List&lt;System.Int32&gt;+Enumerator 类型的迭代器——一个嵌套的 struct,它针对迭代列表进行了优化。

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

      【讨论】:

      • 这不太正确:枚举器是 相同的,但在第二种情况下,由于访问堆中的盒装枚举器(取消引用)和虚拟调用( callvirtcall 在 il)
      【解决方案4】:

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

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

      请检查; For and Foreach comparison

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-07-09
        • 1970-01-01
        • 2010-09-11
        相关资源
        最近更新 更多