【问题标题】:Overhead of Iterating T[] cast to IList<T>迭代 T[] 的开销强制转换为 IList<T>
【发布时间】:2011-11-26 17:53:50
【问题描述】:

我注意到迭代已转换为通用接口集合(IList 或 IEnumberable)的原始集合 (T[]) 会降低性能。

例如:

    private static int Sum(int[] array)
    {
        int sum = 0;

        foreach (int i in array)
            sum += i;

        return sum;
    }

上面的代码执行速度明显快于下面的代码,其中参数更改为 IList 类型(或 IEnumerable):

    private static int Sum(IList<int> array)
    {
        int sum = 0;

        foreach (int i in array)
            sum += i;

        return sum;
    }

如果传递的对象是原始数组,并且我尝试将循环更改为 for 循环而不是 foreach 循环,性能仍然会受到影响。

我可以通过这样的编码来解决性能问题:

    private static int Sum(IList<int> array)
    {
        int sum = 0;

        if( array is int[] )
            foreach (int i in (int[])array)
                sum += i;
        else
            foreach (int i in array)
                sum += i;

        return sum;
    }

有没有更优雅的方法来解决这个问题?感谢您的宝贵时间。

编辑:我的基准代码:

    static void Main(string[] args)
    {
        int[] values = Enumerable.Range(0, 10000000).ToArray<int>();
        Stopwatch sw = new Stopwatch();

        sw.Start();
        Sum(values);
        //Sum((IList<int>)values);
        sw.Stop();

        Console.WriteLine("Elasped: {0} ms", sw.ElapsedMilliseconds);
        Console.Read();
    }

【问题讨论】:

  • 你是如何衡量这个的?如果你在循环中重复运行代码,VM可能会动态优化它。
  • 我没有在虚拟机中运行它。我会将我的简单基准代码添加到问题中。
  • 我还会考虑三元 for 循环并使用索引器进行交互。有趣...
  • 性能会受到多大影响?通常在实践中,这种性能差异对实际处理部分没有任何意义。我敢肯定,在你的实际项目中,你不会做简单的 sum+=i,任何更高级的处理都会比 IList 迭代花费更长的时间,即使它确实比 int[] 迭代需要更多的时间。跨度>
  • 我自己检查了速度。看起来,有一个表演命中,它是戏剧性的100%!!!! :) 对于 100000000 个 int 数组,在我的 Core 2 Duo E8200 上需要 1.2 秒,而 IList 需要 2.4 秒。我真的会忘记这种差异。 PS 我在调试时使用了作者提供的方法。

标签: c# performance generics collections


【解决方案1】:

如果此方法对性能至关重要,则最好的办法是使用 int[] 作为参数为 Sum 创建重载。 CLR 的 JIT 可以检测 foreach 样式的数组迭代,并且可以跳过范围检查并直接寻址每个元素。在 x86 上,循环的每次迭代需要 3-5 条指令,只需要一次内存查找。

使用 IList 时,JIT 不了解底层集合的结构,最终使用 IEnumerator&lt;int&gt;。循环的每次迭代都使用两个接口调用——一个用于Current,一个用于MoveNext(2-3 个内存查找和每个调用)。这很可能以大约 20 条非常昂贵的指令告终,而且您无能为力。

编辑:如果您对 JIT 从发布版本没有附加调试器发出的实际机器代码感到好奇,这里就是。

数组版本

            int s = 0;
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  xor         esi,esi 
            foreach (int i in arg)
00000007  xor         edx,edx 
00000009  mov         edi,dword ptr [ecx+4] 
0000000c  test        edi,edi 
0000000e  jle         0000001B 
00000010  mov         eax,dword ptr [ecx+edx*4+8] 
                s += i;
00000014  add         esi,eax 
00000016  inc         edx  
            foreach (int i in arg)
00000017  cmp         edi,edx 
00000019  jg          00000010 

IEnumerable 版本

            int s = 0;
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  
00000006  sub         esp,1Ch 
00000009  mov         esi,ecx 
0000000b  lea         edi,[ebp-28h] 
0000000e  mov         ecx,6 
00000013  xor         eax,eax 
00000015  rep stos    dword ptr es:[edi] 
00000017  mov         ecx,esi 
00000019  xor         eax,eax 
0000001b  mov         dword ptr [ebp-18h],eax 
0000001e  xor         edx,edx 
00000020  mov         dword ptr [ebp-24h],edx 
            foreach (int i in arg)
00000023  call        dword ptr ds:[009E0010h] 
00000029  mov         dword ptr [ebp-28h],eax 
0000002c  mov         ecx,dword ptr [ebp-28h] 
0000002f  call        dword ptr ds:[009E0090h] 
00000035  test        eax,eax 
00000037  je          00000052 
00000039  mov         ecx,dword ptr [ebp-28h] 
0000003c  call        dword ptr ds:[009E0110h] 
                s += i;
00000042  add         dword ptr [ebp-24h],eax 
            foreach (int i in arg)
00000045  mov         ecx,dword ptr [ebp-28h] 
00000048  call        dword ptr ds:[009E0090h] 
0000004e  test        eax,eax 
00000050  jne         00000039 
00000052  mov         dword ptr [ebp-1Ch],0 
00000059  mov         dword ptr [ebp-18h],0FCh 
00000060  push        0F403BCh 
00000065  jmp         00000067 
00000067  cmp         dword ptr [ebp-28h],0 
0000006b  je          00000076 
0000006d  mov         ecx,dword ptr [ebp-28h] 
00000070  call        dword ptr ds:[009E0190h] 

【讨论】:

  • 这是我最初的计划,但问题是调用 Sum() 的人可能不知道底层集合是 int[] 还是 List。无论哪种方式,我都必须在 Sum(IList) 中检查它是否是 int[],否则我会在每次 Sum 调用之前进行 if 检查。
  • 要明确:调用 Sum((IList)array) 运行速度比 Sum(array) 与重载慢,那么如果正确的重载可能不是,那么重载的目的是什么执行了吗?
  • 如果你想不惜一切代价避免类型检查,你可以在你的方法参数上使用ToArray(),并且每次都迭代int[]。对于 int[] 参数,您最终会创建数组的副本,而其他实现将同时存在 IEnumerator&lt;int&gt; 迭代和数组创建。无论哪种方式 - 性能损失和内存压力都不值得。如果您对此感到羞耻,最好使用is 并将其隐藏在#region 中:-)
  • 保持Sum(IList&lt;int&gt;) 的要点是为将您的代码与其他集合一起使用的人提供后备。当然,如果它是没人会使用的私有辅助方法,则只保留方便的实现。
  • 我将把它标记为答案,但我想我要么会得到冗余代码,要么会在循环中调用方法;没那么优雅。
【解决方案2】:

欢迎优化。这里的事情并不总是显而易见的!

基本上,正如您所发现的,当编译器检测到某些类型的安全约束被证明是有效的时,它可以在进行全面优化时发出更高效的代码。在这里(如 MagnatLU 所示),我们看到知道您有一个数组允许对固定大小做出各种假设,并且它允许直接访问内存(这在它与CPU 的内存预取代码,用于提高速度)。当编译器没有证据证明它可以生成超快代码时,它会很安全。 (这是正确的做法。)

作为一般评论,当涉及到优化编码时,您的解决方法代码非常简单(当使代码具有超级可读性和可维护性时,并不总是首先考虑的因素)。我真的不明白如何在不让你的类的 API 更复杂的情况下改进它(不是胜利!)。此外,只需在正文中添加注释说明您这样做的原因即可解决维护问题;实际上,这首先是代码中(非文档)cmets 的最佳用途之一。鉴于用例是大型数组(即,首先优化是合理的)我会说你有一个很好的解决方案。

【讨论】:

    【解决方案3】:

    作为上述@MagnatLU 答案的替代方案,您可以使用for 而不是foreach 并缓存列表的Count。与int[] 相比,仍然存在开销,但没有那么多:在我的机器上使用您的测试代码,IList&lt;int&gt; 的过载持续时间减少了约 50%。

    private static int Sum(IList<int> array)
    {
        int sum = 0;
    
        int count = array.Count;
        for (int i = 0; i < count; i++)
            sum += array[i];
    
        return sum;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多