迭代T[]的开销强制转换为IList
本文关键字:转换 IList 开销 迭代 | 更新日期: 2023-09-27 18:15:53
我注意到在被转换为泛型接口集合(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();
}
如果该方法对性能至关重要,最好的方法是为Sum
创建过载,并将int[]
作为参数。CLR的JIT可以检测数组上的foreach样式的迭代,并且可以跳过范围检查并直接寻址每个元素。在x86上,每次循环迭代需要3-5条指令,只需要一次内存查找。
使用IList时,JIT不知道底层集合的结构,最终使用IEnumerator<int>
。循环的每次迭代使用两个接口调用—一个用于Current
,一个用于MoveNext
(2-3次内存查找和对每个接口的调用)。这很可能会导致大约20条非常昂贵的指令,而且您对此无能为力。
Edit:如果您对没有附加调试器的Release build 中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]
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
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]
欢迎光临优化。事情并不总是显而易见的!
基本上,正如您所发现的那样,当编译器检测到某些类型的安全约束被证明可以保存时,它可以在进行完全优化时发出非常高效的代码。在这里(如MagnatLU所示),我们看到,知道您已经有了一个数组,就可以对固定的大小做出各种假设,并且它允许直接访问内存(这在如何与CPU的内存预取代码集成方面也是最有效的,以获得额外的速度)。当编译器没有证据证明它可以生成超快的代码时,它就会采取安全策略。(这是正确的做法。)作为一般评论,当涉及到优化编码时,您的工作区代码非常简单(当使代码具有超级可读性和可维护性并不总是首先考虑的时候)。如果不让类的API变得更复杂,我真的不知道如何改进它(这不是一个胜利!)此外,只需在代码体中添加注释,说明为什么这样做,就可以解决维护问题;这实际上是代码中(非文档)注释的最佳用途之一。考虑到用例是大数组(即,首先优化是合理的),我想说您在那里有一个很好的解决方案。
作为@MagnatLU上面的答案的替代方案,您可以使用for
代替foreach
并缓存列表的Count
。与int[]
相比,仍然有开销,但没有那么多:在我的机器上使用您的测试代码,IList<int>
的过载持续时间减少了约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;
}