8

私は最近、C# で for ループと foreach ループのパフォーマンスをテストしており、int の配列を long に合計する場合、foreach ループの方が実際には高速になる可能性があることに気付きました。これが完全なテスト プログラムです。Visual Studio 2012、x86、リリース モード、最適化を使用しました。

両方のループのアセンブリ コードを次に示します。foreach:

            long sum = 0;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  xor         ebx,ebx 
00000008  xor         edi,edi 
            foreach (var i in collection) {
0000000a  xor         esi,esi 
0000000c  cmp         dword ptr [ecx+4],0 
00000010  jle         00000025 
00000012  mov         eax,dword ptr [ecx+esi*4+8] 
                sum += i;
00000016  mov         edx,eax 
00000018  sar         edx,1Fh 
0000001b  add         ebx,eax 
0000001d  adc         edi,edx 
0000001f  inc         esi 
            foreach (var i in collection) {
00000020  cmp         dword ptr [ecx+4],esi 
00000023  jg          00000012 
            }
            return sum;
00000025  mov         eax,ebx 
00000027  mov         edx,edi 
00000029  pop         ebx 
0000002a  pop         esi 
0000002b  pop         edi 
0000002c  pop         ebp 
0000002d  ret 

そして、次の場合:

    long sum = 0;
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  push        eax 
00000007  xor         ebx,ebx 
00000009  xor         edi,edi 
            for (int i = 0; i < collection.Length; ++i) {
0000000b  xor         esi,esi 
0000000d  mov         eax,dword ptr [ecx+4] 
00000010  mov         dword ptr [ebp-10h],eax 
00000013  test        eax,eax 
00000015  jle         0000002A 
                sum += collection[i];
00000017  mov         eax,dword ptr [ecx+esi*4+8] 
0000001b  cdq 
0000001c  add         eax,ebx 
0000001e  adc         edx,edi 
00000020  mov         ebx,eax 
00000022  mov         edi,edx 
            for (int i = 0; i < collection.Length; ++i) {
00000024  inc         esi 
00000025  cmp         dword ptr [ebp-10h],esi 
00000028  jg          00000017 
            }
            return sum;
0000002a  mov         eax,ebx 
0000002c  mov         edx,edi 
0000002e  pop         ecx 
0000002f  pop         ebx 
00000030  pop         esi 
00000031  pop         edi 
00000032  pop         ebp 
00000033  ret

ご覧のとおり、メインループは foreach が 7 命令、for が 9 命令です。これは、私のベンチマークでは約 10% のパフォーマンスの違いに相当します。

しかし、私はアセンブリ コードを読むのが苦手で、for ループが少なくとも foreach ほど効率的ではない理由がわかりません。ここで何が起こっているのですか?

4

3 に答える 3

8

配列が非常に大きいため、唯一の関連部分は明らかにループ内の部分です。これは次のとおりです。

// for loop
00000017  mov         eax,dword ptr [ecx+esi*4+8] 
0000001b  cdq 
0000001c  add         eax,ebx 
0000001e  adc         edx,edi 
00000020  mov         ebx,eax 
00000022  mov         edi,edx 

// foreach loop
00000012  mov         eax,dword ptr [ecx+esi*4+8] 
00000016  mov         edx,eax 
00000018  sar         edx,1Fh 
0000001b  add         ebx,eax 
0000001d  adc         edi,edx 

合計は long int であるため、2 つの異なるレジスタに格納されます。つまり、ebx には最下位 4 バイトが含まれ、edi には最上位 4 バイトが含まれます。collection[i] が (暗黙的に) int から long にキャストされる方法が異なります。

// for loop
0000001b  cdq 

// foreach loop
00000016  mov         edx,eax 
00000018  sar         edx,1Fh 

注意すべきもう 1 つの重要な点は、for ループ バージョンが「逆」の順序で合計を実行することです。

long temp = (long) collection[i];   // implicit cast, stored in edx:eax
temp += sum;                        // instead of "simply" sum += temp
sum = temp;                         // sum is stored back into ebx:edi

コンパイラが sum += temp の代わりにこの方法を好んだ理由はわかりません (@EricLippert が教えてくれるかもしれません :) ) が、発生する可能性のある命令の依存関係の問題に関連していると思われます。

于 2013-01-10T17:26:31.700 に答える
6

OK、ループ内の命令が非常に近いことがわかるように、これがアセンブリコードの注釈付きバージョンです。

            foreach (var i in collection) {
0000000a  xor         esi,esi                       clear index
0000000c  cmp         dword ptr [ecx+4],0           get size of collection
00000010  jle         00000025                      exit if empty
00000012  mov         eax,dword ptr [ecx+esi*4+8]   get item from collection
                sum += i;
00000016  mov         edx,eax                       move to edx:eax
00000018  sar         edx,1Fh                       shift 31 bits to keep sign only
0000001b  add         ebx,eax                       add to sum
0000001d  adc         edi,edx                       add with carry from previous add
0000001f  inc         esi                           increment index
            foreach (var i in collection) {
00000020  cmp         dword ptr [ecx+4],esi         compare size to index
00000023  jg          00000012                      loop if more
            }
            return sum;
00000025  mov         eax,ebx                       result was in ebx
=================================================
            for (int i = 0; i < collection.Length; ++i) {
0000000b  xor         esi,esi                       clear index
0000000d  mov         eax,dword ptr [ecx+4]         get limit on for
00000010  mov         dword ptr [ebp-10h],eax       save limit
00000013  test        eax,eax                       test if limit is empty
00000015  jle         0000002A                      exit loop if empty
                sum += collection[i];
00000017  mov         eax,dword ptr [ecx+esi*4+8]   get item form collection  
0000001b  cdq                                       convert eax to edx:eax
0000001c  add         eax,ebx                       add to sum
0000001e  adc         edx,edi                       add with carry from previous add
00000020  mov         ebx,eax                       put result in edi:ebx
00000022  mov         edi,edx 
            for (int i = 0; i < collection.Length; ++i) {
00000024  inc         esi                           increment index
00000025  cmp         dword ptr [ebp-10h],esi       compare to limit
00000028  jg          00000017                      loop if more
            }
            return sum;
0000002a  mov         eax,ebx                       result was in ebx
于 2013-01-10T16:50:25.603 に答える
-1

C# 言語仕様 4.0によると、foreachループはコンパイラによって次のように分割されます。

foreach ステートメント:

foreach ( 式内 のローカル変数型 識別子 ) 埋め込みステートメント

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
        while (e.MoveNext()) {
            v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
        … // Dispose e
    }
}

これは、次の処理の後です (これも仕様から)。

•<strong> expressionの型 X が配列型の場合、X からインターフェイスへの暗黙的な参照変換がありSystem.Collections.IEnumerableます (System.Arrayこのインターフェイスを実装しているため)。コレクション型はSystem.Collections.IEnumerableインターフェイス、列挙子型はSystem.Collections.IEnumeratorインターフェイス、要素型は配列型 X の要素型です。

コンパイラから同じアセンブリ コードが表示されないのは、おそらく正当な理由です。

于 2013-01-10T16:32:01.333 に答える