6
sealed class A
{
    public int X;
    public int Y { get; set; }
}

A の新しいインスタンスを作成すると、Y に 100,000,000 回アクセスするのに約 550 ミリ秒かかりますが、X にアクセスするには約 250 ミリ秒かかります。.NET が Y をフィールドに最適化しないのはなぜですか?

編集:

    A t = new A();
    t.Y = 50;
    t.X = 50;

    Int64 y = 0;

    Stopwatch sw = new Stopwatch();
    sw.Start();

    for (int i = 0; i < 100000000; i++)
        y += t.Y;

    sw.Stop();

これは私がテストに使用しているコードであり、代わりに tY を tX に変更して X をテストしています。また、私はリリースビルドにいます。

4

2 に答える 2

28
for (int i = 0; i < 100000000; i++)
    y += t.X;

これは、プロファイリングが非常に難しいコードです。Debug + Windows + Disassembly で生成されたマシン コードを見ると、それがわかります。x64 コードは次のようになります。

0000005a  xor         r11d,r11d                           ; i = 0
0000005d  mov         eax,dword ptr [rbx+0Ch]             ; read t.X
00000060  add         r11d,4                              ; i += 4
00000064  cmp         r11d,5F5E100h                       ; test i < 100000000
0000006b  jl          0000000000000060                    ; for (;;)

これは大幅に最適化されたコードです。+= 演算子が完全になくなったことに注意してください。ベンチマークで間違いを犯したため、これが発生することを許可しました。計算された y の値をまったく使用していません。ジッターはこれを知っているので、無意味な追加を単純に削除しました。4 ずつインクリメントすることにも説明が必要です。これは、ループ展開の最適化の副作用です。後で使用されます。

したがって、ベンチマークを現実的なものにするために変更を加える必要があります。最後に次の行を追加します。

sw.Stop();
Console.WriteLine("{0} msec, {1}", sw.ElapsesMilliseconds, y);

これにより、y の値が強制的に計算されます。今では完全に異なって見えます:

0000005d  xor         ebp,ebp                             ; y = 0
0000005f  mov         eax,dword ptr [rbx+0Ch]          
00000062  movsxd      rdx,eax                             ; rdx = t.X
00000065  nop         word ptr [rax+rax+00000000h]        ; align branch target
00000070  lea         rax,[rdx+rbp]                       ; y += t.X
00000074  lea         rcx,[rax+rdx]                       ; y += t.X
00000078  lea         rax,[rcx+rdx]                       ; y += t.X
0000007c  lea         rbp,[rax+rdx]                       ; y += t.X
00000080  add         r11d,4                              ; i += 4
00000084  cmp         r11d,5F5E100h                       ; test i < 100000000
0000008b  jl          0000000000000070                    ; for (;;)

まだ非常に最適化されたコードです。奇妙な NOP 命令は、アドレス 008b でのジャンプが効率的であることを保証し、16 にアラインされたアドレスへのジャンプは、プロセッサ内の命令デコーダ ユニットを最適化します。LEA 命令は、アドレス生成ユニットに加算を生成させるための古典的なトリックであり、メイン ALU が同時に他の作業を実行できるようにします。ここで行う作業は他にありませんが、ループ本体がもっと複​​雑な場合は行うことができます。また、ループは分岐命令を避けるために 4 回展開されました。

とにかく、削除されたコードではなく、実際のコードを実際に測定していることになります。テストを 10 回繰り返した私のマシンでの結果 (重要!):

y += t.X: 125 msec
y += t.Y: 125 msec

まったく同じ時間です。もちろん、その通りであるべきです。物件にお金はかかりません。

ジッタは、高品質のマシン コードを生成する優れた仕事をします。奇妙な結果が得られた場合は、常に最初にテスト コードを確認してください。一番間違いやすいコードです。ジッターではなく、徹底的にテストされています。

于 2013-03-16T21:48:49.937 に答える
5

X単純なフィールドです。ただし、名前付きおよび内部的には、およびアクセサー Yを持つプロパティです。特別なコンパイラ生成名を持つプライベートバッキング フィールドもあり、アクセサーはバッキング フィールドにアクセスします。実際に示されている次の画像:getsetint get_Y()void set_Y(int)Y

uplyZ.jpg

これは、C# 言語仕様に従って、コンパイラが行うべき方法です。C# コンパイラが代わりにフィールドを発行した場合、仕様に違反します。

もちろん、ランタイムは、コンパイラによって生成されたアクセサーを使用する必要があります。ただし、ランタイムは、アクセサーへの余分な呼び出しを回避するために、インライン化などのトリックを実行する場合があります。これは、プロパティへのアクセスをフィールドへのアクセスと同じくらい高速にする最適化です。

Hans Passantは、実際にはランタイムプロパティへのアクセスを同じくらい高速に行うことを強調しています。元のテスト コードに欠陥があり、割り当てられたローカル変数が使用されなかったため、ランタイムは読み取りを削除できました。詳細については、Passant の回答を参照してください。

それでも、単純なフィールドが必要な場合は、作成し、自動プロパティを作成しないでください。

于 2013-03-16T20:46:01.743 に答える