6

Cでは、x86マシンにコンパイルすると、条件が複雑な場合でも、たとえば、速度が最も重要な側面である場合、通常、ブランチを論理式に置き換えます。

char isSomething() {
    if (complexExpression01) {
        if (complexExpression02) {
            if(!complexExpression03) {
                return 1;
            }
        }
    }
    return 0;
}

私は書く:

char isSomething() {
    return complexExpression01 &&
           complexExpression02 &&
           !complexExpression03 ;
}

明らかに、これは保守が難しく、コードが読みにくくなる可能性がありますが、実際には高速である可能性があります。

C#などのマネージコードを操作するときに同じように動作する理由はありますか?「ジャンプ」は、マネージコードでは(少なくともx86では)マネージコードであるため、コストがかかりますか?

4

4 に答える 4

4

全般的

通常のコンパイラでは、少なくとも通常のコンパイラを使用していると仮定した場合、生成されるコードはほとんどの場合同じになります。

csc.exe /optimize+
cl.exe /O2
g++ -O2

および関連するデフォルトの最適化モード。

一般的なマントラは、プロファイル、プロファイル、プロファイルです(そして、プロファイラーから指示されるまで、マイクロ最適化しないでください)。生成されたコード2をいつでも見て、改善の余地があるかどうかを確認できます。

このように考えてください。たとえば、C#コード:

C#/。NET

complexExpressionsは、引数をスタックにプッシュする必要がある事実上の関数呼び出し呼び出し(call、calli、callvirt opcode 3 )です。戻り値は、終了時のパラメーターではなく、スタックにプッシュされたままになります。

現在、CLRはスタックベースの仮想マシン(つまりレジスタレス)であるため、これはスタック上の匿名の一時変数とまったく同じになります。唯一の違いは、コードで使用される識別子の数です。

JITエンジンがそれをどのように処理するかは別の問題です。JITエンジンはこれらの呼び出しをネイティブアセンブリに変換する必要があり、レジスタ割り当て、命令の順序付け、分岐予測などを微調整することで最適化を行う可能性があります1

1(実際には、このサンプルでは、complex function calls​​副作用が発生する可能性があり、C#仕様が評価順序といわゆるシーケンスについて非常に明確であるため、より興味深い最適化を行うことはできません)。ただし、呼び出しのオーバーヘッドを減らすために、JITエンジンは関数呼び出しをインライン化できることに注意してください。

それらが非仮想である場合だけでなく、(IIRC)特定の.NETFramework内部のコンパイル時にランタイムタイプを静的に知ることができる場合もあります。これについてはリファレンスを調べる必要がありますが、実際には、フレームワーク関数のインライン化を明示的に防ぐために.NETFramework4.0で導入された属性があると思います。これは、ユーザーアセンブリが事前にネイティブイメージにコンパイルされている場合でも、Microsoftがサービスパック/更新プログラムのライブラリコードにパッチを適用できるようにするためです。

C / C ++

C / C ++では、メモリモデルははるかに緩く(つまり、少なくともC ++ 11まで)、コードは通常、コンパイル時に直接ネイティブ命令にコンパイルされます。C / C ++コンパイラは通常、積極的なインライン化を行うことを追加します。最適化を有効にせずにコンパイルしない限り、そのようなコンパイラでもコードは通常同じになります。


2私は使用します

  • ildasmまたはmonodis生成されたILコードを確認するには
  • mono -aot=full,staticまたはmkbundle、ネイティブオブジェクトモジュールを生成しobjdump -CdS、そのための注釈付きネイティブアセンブリ命令を表示します。

これは純粋に好奇心であることに注意してください。そのように興味深いボトルネックを見つけることはめったにないからです。ただし、ジェネリッククラス用に生成されたILコードに潜んでいる可能性のある驚きの良い例については、パフォーマンスの最適化に関するSkeetのブログ投稿のNoda.NETJを参照してください。

3コンパイラ組み込み関数の演算子の 編集は正確ではありませんが、結果はスタックに残されます。

于 2011-11-08T07:51:52.707 に答える
2

これは、管理対象言語のCLRとコンパイラの実装次第です。C#の場合、次のテストケースは、ネストされたifステートメントと結合されたifステートメントの命令に違いがないことを証明します。

            // case 1
            if (value1 < value2)
00000089  mov         eax,dword ptr [ebp-0Ch] 
0000008c  cmp         eax,dword ptr [ebp-10h] 
0000008f  jge         000000A6 
            {
                if (value2 < value3)
00000091  mov         eax,dword ptr [ebp-10h] 
00000094  cmp         eax,dword ptr [ebp-14h] 
00000097  jge         000000A6 
                {
                    result1 = true;
00000099  mov         eax,1 
0000009e  and         eax,0FFh 
000000a3  mov         dword ptr [ebp-4],eax 
                }
            }

            // case 2
            if (value1 < value2 && value2 < value3)
000000a6  mov         eax,dword ptr [ebp-0Ch] 
000000a9  cmp         eax,dword ptr [ebp-10h] 
000000ac  jge         000000C3 
000000ae  mov         eax,dword ptr [ebp-10h] 
000000b1  cmp         eax,dword ptr [ebp-14h] 
000000b4  jge         000000C3 
            {
                result2 = true;
000000b6  mov         eax,1 
000000bb  and         eax,0FFh 
000000c0  mov         dword ptr [ebp-8],eax 
            }
于 2011-11-08T08:12:03.160 に答える
1

&&論理演算子( )はCとC#の両方で短絡セマンティクスを持っているため、2つの式は同じ数のテストになります。したがって、質問の前提(プログラムを表現する2番目の方法では分岐が少なくなる)は正しくありません。

于 2011-11-08T07:59:57.733 に答える
0

知る唯一の方法は測定することです。

真と偽はCLRによって1と0として表されるため、論理式を使用することに利点があったとしても、私は驚かないでしょう。どれどれ:

static void BenchBranch() {
    Stopwatch sw = new Stopwatch();

    const int NMAX = 1000000000;
    bool a = true;
    bool b = false;
    bool c = true;

    sw.Restart();
    int sum = 0;
    for (int i = 0; i < NMAX; i++) {
        if (a)
            if (b)
                if (c)
                    sum++;
        a = !a;
        b = a ^ b;
        c = b;
    }
    sw.Stop();
    Console.WriteLine("1: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);

    sw.Restart();
    sum = 0;
    for (int i = 0; i < NMAX; i++) {
        if (a && b && c) 
            sum++;
        a = !a;
        b = a ^ b;
        c = b;
    }
    sw.Stop();
    Console.WriteLine("2: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);

    sw.Restart();
    sum = 0;
    for (int i = 0; i < NMAX; i++) {
        sum += (a && b && c) ? 1 : 0;
        a = !a;
        b = a ^ b;
        c = b;
    }
    sw.Stop();
    Console.WriteLine("3: {0:F3} ms ({1})", sw.Elapsed.TotalMilliseconds, sum);
}

結果:

1:  2713.396 ms (250000000)
2:  2477.912 ms (250000000)
3:  2324.916 ms (250000000)

したがって、このことから、ネストされた条件ステートメントの代わりに論理演算子を使用することにはわずかな利点があるようです。ただし、特定のインスタンスでは、結果が多少異なる場合があります。

結局、このようなマイクロ最適化が価値があるかどうかは、コードのパフォーマンスがどれほど重要かによって異なります。

于 2011-11-08T08:13:39.450 に答える