44

この for ループの最初の 2 行をコメント アウトし、3 番目のコメントを解除すると、42% 高速化されるのはなぜですか?

int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
    var isMultipleOf16 = i % 16 == 0;
    count += isMultipleOf16 ? 1 : 0;
    //count += i % 16 == 0 ? 1 : 0;
}

タイミングの背後には、大幅に異なるアセンブリ コードがあります。ループ内の命令は 13 対 7 です。プラットフォームは、.NET 4.0 x64 を実行する Windows 7 です。コードの最適化が有効になっており、テスト アプリが VS2010 の外部で実行されました。[更新: Repro project、プロジェクト設定の検証に役立ちます。]

中間ブール値を排除することは基本的な最適化であり、私の 1980 年代のDragon Bookで最も単純なものの 1 つです。CIL の生成時または x64 マシン コードの JIT 時に最適化が適用されなかったのはなぜですか?

「本当にコンパイラ、このコードを最適化してください」スイッチはありますか? 時期尚早の最適化はお金への愛情に似ているという意見には同情しますが、ルーチン全体にこのような問題が散在する複雑なアルゴリズムをプロファイリングしようとすると、フラストレーションがたまることがわかりました。ホットスポットを処理することはできますが、通常はコンパイラーから当然と思われていることを手動で微調整することで大幅に改善できる、より広いウォーム領域のヒントはありません。ここで何かが欠けていることを願っています。

更新:速度の違いは x86 でも発生しますが、メソッドがジャストインタイムでコンパイルされる順序によって異なります。JIT オーダーがパフォーマンスに影響する理由を参照してください。

アセンブリ コード(要求に応じて):

    var isMultipleOf16 = i % 16 == 0;
00000037  mov         eax,edx 
00000039  and         eax,0Fh 
0000003c  xor         ecx,ecx 
0000003e  test        eax,eax 
00000040  sete        cl 
    count += isMultipleOf16 ? 1 : 0;
00000043  movzx       eax,cl 
00000046  test        eax,eax 
00000048  jne         0000000000000050 
0000004a  xor         eax,eax 
0000004c  jmp         0000000000000055 
0000004e  xchg        ax,ax 
00000050  mov         eax,1 
00000055  lea         r8d,[rbx+rax] 
    count += i % 16 == 0 ? 1 : 0;
00000037  mov         eax,ecx 
00000039  and         eax,0Fh 
0000003c  je          0000000000000042 
0000003e  xor         eax,eax 
00000040  jmp         0000000000000047 
00000042  mov         eax,1 
00000047  lea         edx,[rbx+rax] 
4

5 に答える 5

9

質問は、「自分のマシンでこのような違いが見られるのはなぜですか?」です。このような大きな速度差を再現することはできず、お使いの環境に固有のものがあると思われます。それが何であるかを言うのは非常に難しいです。少し前に設定したいくつかの(コンパイラ)オプションであり、それらを忘れている可能性があります。

コンソール アプリケーションを作成し、リリース モード (x86) で再構築して、VS の外部で実行しました。結果はほぼ同じで、どちらの方法でも 1.77 秒です。正確なコードは次のとおりです。

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    int count = 0;

    for (uint i = 0; i < 1000000000; ++i)
    {
        // 1st method
        var isMultipleOf16 = i % 16 == 0;
        count += isMultipleOf16 ? 1 : 0;

        // 2nd method
        //count += i % 16 == 0 ? 1 : 0;
    }

    sw.Stop();
    Console.WriteLine(string.Format("Ellapsed {0}, count {1}", sw.Elapsed, count));
    Console.ReadKey();
}

5 分以内にコードをコピーし、再構築し、VS の外部で実行して、結果をこの回答へのコメントに投稿してください。「私のマシンで動作する」と言うのは避けたいです。

編集

64ビットのWinformsアプリケーションを作成し、結果が質問と同様であることを確認するために、最初の方法は2番目の方法(1.05秒)よりも遅い(1.57秒)。私が観察した違いは 33% で、それでもかなり大きいです。.NET4 64 ビット JIT コンパイラにバグがあるようです。

于 2012-04-30T09:46:59.077 に答える
4

.NET コンパイラ、その最適化、さらにはいつ最適化を実行するのかについて話すことはできません。

しかし、この特定のケースでは、コンパイラがそのブール変数を実際のステートメントに組み込み、このコードをデバッグしようとすると、最適化されたコードは記述されたコードと一致しません。isMulitpleOf16 割り当てを 1 ステップ オーバーして値を確認することはできません。

これは、最適化をオフにする可能性のある場所の一例にすぎません。他にもあるかもしれません。最適化は、CLR からのコード生成フェーズではなく、コードのロード フェーズで行われる場合があります。

最新のランタイムは非常に複雑です。特に、JIT と動的最適化を実行時に投入する場合はなおさらです。時々、コードが言うことを実行してくれることに感謝しています。

于 2012-04-29T04:20:10.837 に答える
3

これは.NETFrameworkのバグです。

まあ、本当に私は推測しているだけですが、Microsoft Connectにバグレポートを提出して、彼らが何を言っているかを確認しました。Microsoftがそのレポートを削除した後、GitHubのroslynプロジェクトでレポートを再送信しました。

更新: Microsoftはこの問題をcoreclrプロジェクトに移動しました。この問題に関するコメントから、それをバグと呼ぶのは少し強いようです。最適化が欠けているということです。

于 2012-04-30T11:39:18.830 に答える
2

これはあなたの他の質問に関連していると思います。次のようにコードを変更すると、複数行のバージョンが優先されます。

おっと、x86 でのみ。x64 では、複数行が最も遅く、条件付きが両方を簡単に打ち負かします。

class Program
{
    static void Main()
    {
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
    }

    public static void ConditionalTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            if (i % 16 == 0) ++count;
        }
        stopwatch.Stop();
        Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void SingleLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            count += i % 16 == 0 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void MultiLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            var isMultipleOf16 = i % 16 == 0;
            count += isMultipleOf16 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Multi-line test  --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }
}
于 2012-05-02T04:01:41.323 に答える
1

私はこのように考える傾向があります: コンパイラに取り組む人々は、年間にできることは限られています。その間にラムダや多くの古典的な最適化を実装できるなら、私はラムダに投票します。C# は、実行時間という点ではなく、コードの読み書きの労力という点で効率的な言語です。

そのため、チームが、特定のコーナー ケース (おそらく何千もあるケース) での実行効率ではなく、読み取り/書き込み効率を最大化する機能に集中することは合理的です。

最初は、JITter がすべての最適化を行うという考えだったと思います。残念ながら、JITting にはかなりの時間がかかり、高度な最適化を行うとさらに悪化します。そのため、期待したほどうまくいきませんでした。

C# で非常に高速なコードをプログラミングすることについて私が発見したことの 1 つは、あなたが言及したような最適化が違いを生む前に、深刻な GC ボトルネックに遭遇することが非常に多いということです。何百万ものオブジェクトを割り当てる場合と同様です。C# は、コストを回避するという点でほとんど残されていません。代わりに構造体の配列を使用できますが、結果のコードは比較すると非常に見苦しくなります。私の言いたいことは、C# と .NET に関する他の多くの決定は、C++ コンパイラのようなものよりも、そのような特定の最適化を価値のないものにするということです。一体、彼らは NGENで CPU 固有の最適化を削除し、プログラマー (デバッガー) の効率とパフォーマンスを交換しました。

とはいえ、C++ が 1990 年代から利用していた最適化を実際に利用した C# が大好きです。たとえば、async/await などの機能を犠牲にするわけではありません。

于 2012-05-06T19:23:06.143 に答える