2017-02-08 5 views
2

マシンでリリースモードで次のコードを実行すると、デリゲートにnullターゲットがある場合よりも、デリゲートがnullターゲットを持つ場合よりもやや速くなります同等かそれより遅くなるように)。.netターゲットのターゲットよりも遅いデリゲート

私はマイクロ最適化を実際に探していませんが、なぜこれが当てはまるのでしょうか?

static void Main(string[] args) 
{ 
    // Warmup code 

    long durationWithTarget = 
     MeasureDuration(() => new DelegatePerformanceTester(withTarget: true).Run()); 

    Console.WriteLine($"With target: {durationWithTarget}"); 

    long durationWithoutTarget = 
     MeasureDuration(() => new DelegatePerformanceTester(withTarget: false).Run()); 

    Console.WriteLine($"Without target: {durationWithoutTarget}"); 
} 

/// <summary> 
/// Measures the duration of an action. 
/// </summary> 
/// <param name="action">Action which duration has to be measured.</param> 
/// <returns>The duration in milliseconds.</returns> 
private static long MeasureDuration(Action action) 
{ 
    Stopwatch stopwatch = Stopwatch.StartNew(); 

    action(); 

    return stopwatch.ElapsedMilliseconds; 
} 

class DelegatePerformanceTester 
{ 
    public DelegatePerformanceTester(bool withTarget) 
    { 
     if (withTarget) 
     { 
      _func = AddNotStatic; 
     } 
     else 
     { 
      _func = AddStatic; 
     } 
    } 
    private readonly Func<double, double, double> _func; 

    private double AddNotStatic(double x, double y) => x + y; 
    private static double AddStatic(double x, double y) => x + y; 

    public void Run() 
    { 
     const int loops = 1000000000; 
     for (int i = 0; i < loops; i++) 
     { 
      double funcResult = _func.Invoke(1d, 2d); 
     } 
    } 
} 
+0

重複していますか? http://stackoverflow.com/questions/2082735/performance-of-calling-delegates-vs-methods –

+0

@ダン私はこの質問は直接メソッド呼び出しの比較についてのもう一つの複製ではないと思います対代表者。私の場合、私は代議員の公演だけに興味があります。 –

+1

「私はマイクロ最適化を本当に探していません」次に、なぜマイクロ最適化についての質問をしますか? – Servy

答えて

8

私はこの1つを書きます、その高速なコードを書く気に任意のC#のプログラマには関係するべきその背後にかなりまともなプログラミングのアドバイスがあります。私はマイクロベンチマークの使用について一般的に注意していますが、現代のCPUコアでのコード実行速度の予測不可能性のために、15%以下の差は一般的に統計的に有意ではありません。そこにないものを測定するオッズを減らすための良いアプローチは、キャ​​ッシング効果を取り除き、コードの位置合わせ効果を排除できるようにテストを入れ替えるために少なくとも10回テストを繰り返すことです。

しかし、あなたが見たものは本当ですが、静的メソッドを呼び出すデリゲートは実際には遅いです。この効果はx86コードでは非常に小さいですが、x64コードではかなり悪くなりますので、Project> Properties> Buildタブ> 32ビットとプラットフォームの優先設定の両方を試してみてください。

なぜそれが遅いのかを知るには、ジッタが生成するマシンコードを調べる必要があります。代理人の場合、そのコードはと非常にです。 [デバッグ]> [Windows]> [逆アセンブリ]を選択してコードを見ると、表示されません。また、コードを一歩でも実行することはできません。管理されたデバッガは、コードを隠すように書かれており、コードを表示することを完全に拒否しています。 Visual Studioに「ビジュアル」を戻すテクニックについて説明する必要があります。

私は「スタブ」について少し話しています。スタブは、ジッタが生成するコードに加えて、CLRが動的に作成するマシンコードを少し複雑にしています。スタブはインターフェイスの実装に使用され、クラスのメソッドテーブル内のメソッドの順序がインターフェイスメソッドの順序と一致する必要がないという柔軟性を提供します。そして、彼らはこの質問の主題である代表者にとって重要です。スタブはジャストインタイムコンパイルにも関係します。スタブの初期コードは、ジッタのエントリポイントを指して、呼び出されるとコンパイルされたメソッドを取得します。その後、スタブが置き換えられ、ジットターゲットメソッドが呼び出されます。静的メソッドの呼び出しを遅くするのはスタブであり、静的メソッドのターゲットのスタブはインスタンスメソッドのスタブよりも巧妙です。


スタブを表示するには、デバッガがコードを表示するように挑戦する必要があります。まず、[ツール]> [オプション]> [デバッグ]> [一般]を使用して設定する必要があります。 「Just My Code」チェックボックスをオフにし、「JIT最適化を抑制する」チェックボックスを選択解除します。 VS2015を使用して「管理された互換モードを使用」にチェックを入れると、VS2015デバッガは非常にバグがあり、この種のデバッグに真剣に取り組みます。このオプションは、VS2010マネージドデバッガエンジンを使用するように強制します。リリース構成に切り替えます。プロジェクト>プロパティ>デバッグの順に選択し、[ネイティブコードのデバッグを有効にする]チェックボックスをオンにします。そして、Project> Properties> Buildを選択し、「32ビット優先」チェックボックスをオフにし、「Platform target」をAnyCPUにする必要があります。

Run()メソッドにブレークポイントを設定します。ブレークポイントが最適化されたコードであまり正確ではないことに注意してください。メソッドヘッダーの設定が最適です。ヒットしたら、[デバッグ]> [Windows]> [逆アセンブリ]を使用して、ジッタが生成したマシンコードを確認します。

   funcResult += _func.Invoke(1d, 2d); 
0000001a mov   rax,qword ptr [rsi+8]    ; rax = _func    
0000001e mov   rcx,qword ptr [rax+8]    ; rcx = _func._methodBase (?) 
00000022 vmovsd  xmm2,qword ptr [0000000000000070h] ; arg3 = 2d 
0000002b vmovsd  xmm1,qword ptr [0000000000000078h] ; arg2 = 1d 
00000034 call  qword ptr [rax+18h]     ; call stub 

64ビットのメソッド呼び出しが最初の4つの引数を渡します。デリゲートの呼び出しコールはハスウェルコア上で次のようになり、あなたはまだAVXをサポートしていない古いプロセッサを持っているかどうかを見るものと一致しない場合がありますレジスタでは、追加の引数はスタック(ここではなく)に渡されます。引数が浮動小数点型であるため、XMMレジスタがここで使用されます。この時点で、ジッタは、メソッドが静的であるかインスタンスであるかをまだ知ることができません。このコードは、このコードが実際に実行されるまでは検出できません。違いを隠すのはスタブの仕事です。それはインスタンスメソッドであることを前提としています。そのため、私はarg2とarg3に注釈を付けました。

CALL命令でブレークポイントを設定します(スタブがジッタをもはや指していないようにしてから、もう一度ヒットします)。手で行う必要があります。デバッグ> Windows>を使用して、RAXレジスタの値を登録してコピーします。デバッグ> Windows>メモリ> Memory1と値を貼り付け、その前に "0x"と0x18を追加します。そのウィンドウを右クリックして「8バイト整数」を選択し、最初に表示された値をコピーします。これがスタブコードのアドレスです。

この時点で、管理されたデバッグエンジンはまだ使用されており、スタブコードを見ることはできません。アンマネージデバッグエンジンが制御されるように、モード切り替えを強制する必要があります。デバッグ> Windows>コールスタックを使用し、RtlUserThreadStartのように下部のメソッド呼び出しをダブルクリックします。デバッガにエンジンの切り替えを強制します。今すぐ行くことができますし、アドレスボックスにアドレスを貼り付けることができます、その前に "0x"を入れてください。 Outはスタブコードをポップします。

00007FFCE66D0100 jmp   00007FFCE66D0E40 

非常に単純なもので、デリゲートターゲットメソッドへの直接ジャンプです。これは高速なコードになります。ジッターはインスタンスメソッドで正しく推測され、デリゲートオブジェクトはRCXレジスタに引数をすでに提供していたため、特別な処理は必要ありません。

2番目のテストに進み、インスタンス呼び出しのスタブを見るために全く同じことをします。今、スタブは非常に異なっている:

000001FE559F0850 mov   rax,rsp     ; ? 
000001FE559F0853 mov   r11,rcx     ; r11 = _func (?) 
000001FE559F0856 movaps  xmm0,xmm1    ; shuffle arg3 into right register 
000001FE559F0859 movaps  xmm1,xmm2    ; shuffle arg2 into right register 
000001FE559F085C mov   r10,qword ptr [r11+20h] ; r10 = _func.Method 
000001FE559F0860 add   r11,20h     ; ? 
000001FE559F0864 jmp   r10      ; jump to _func.Method 

コードは、Microsoftは、おそらくここに、より良い仕事をすることができ、最適なビットグラグラとではない、と私はそれを正しく注釈付き100%わかりません。私は、不要なmov rax、rsp命令は、4つ以上の引数を持つメソッドのスタブにのみ関連していると思います。なぜadd命令が必要なのか分かりません。 XMMレジスタの移動に関する重要な詳細は、静的メソッドにはthis引数がないため、再シャッフルする必要があります。コードを遅くするのは、この改革の必要条件です。

04F905B4 mov   eax,ecx 
04F905B6 add   eax,10h 
04F905B9 jmp   dword ptr [eax]  ; jump to _func.Method 

32ビットコードが罹患していない理由である64ビットスタブ、よりはるかに簡単:

あなたは、x86ジッタと同じ運動を行うことができ、静的メソッドスタブは今のようになります。減速はほぼ同じくらい。非常に異なる1つの理由は、32ビットコードがFPUスタック上の浮動小数点を渡し、再配置する必要がないことです。積分またはオブジェクトの引数を使用すると、必ずしも高速になるとは限りません。


私はまだ誰も眠らないと思っています。私はいくつかの注釈が間違っているかもしれないことに注意してください。私はスタブとCLRがオブジェクトメンバーを代理してできるだけ速くコードを作る方法を完全に理解していません。しかしここにはまともなプログラミングアドバイスがあります。あなたは実際にデリゲートターゲットとしてインスタンスメソッドを優先させ、staticでなく、の最適化を行います。

+0

これをもっと明確にするために多くの、多くのありがとう。それでなぜ2つの違いがあるのか​​が完全に分かります。そして、あなたを証明するために、あなたは私を寝かせませんでした。「代理人の目標への直接ジャンプ」。私はあなたが 'デリゲートメソッドへの直接ジャンプ'を意味すると思います。再度、感謝します –

関連する問題