53

私は現在、主に楽しみと学習のためにいくつかの最後の手段の最適化を行っていますが、いくつかの質問が残っている何かを発見しました。

まず、質問:

  1. DynamicMethodを使用してメモリ内にメソッドを構築し、デバッガーを使用する場合、逆アセンブラービューでコードを表示するときに、生成されたアセンブリコードにステップインする方法はありますか?デバッガーは私にとってメソッド全体をステップオーバーするようです
  2. または、それが不可能な場合は、生成されたILコードをアセンブリとしてディスクに保存して、Reflectorで検査できるようにすることはできますか?
  3. Expression<...>単純な加算メソッドのバージョン(Int32 + Int32 => Int32)が、最小のDynamicMethodバージョンよりも高速に実行されるのはなぜですか?

これは、デモンストレーションを行う短くて完全なプログラムです。私のシステムでは、出力は次のとおりです。

DynamicMethod: 887 ms
Lambda: 1878 ms
Method: 1969 ms
Expression: 681 ms

ラムダとメソッドの呼び出しの値が高くなると予想しましたが、DynamicMethodのバージョンは一貫して約30〜50%遅くなっています(おそらくWindowsや他のプログラムによる変動)。誰もが理由を知っていますか?

プログラムは次のとおりです。

using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;

namespace Sandbox
{
    public class Program
    {
        public static void Main(String[] args)
        {
            DynamicMethod method = new DynamicMethod("TestMethod",
                typeof(Int32), new Type[] { typeof(Int32), typeof(Int32) });
            var il = method.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Ret);

            Func<Int32, Int32, Int32> f1 =
                (Func<Int32, Int32, Int32>)method.CreateDelegate(
                    typeof(Func<Int32, Int32, Int32>));
            Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
            Func<Int32, Int32, Int32> f3 = Sum;
            Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
            Func<Int32, Int32, Int32> f4 = f4x.Compile();
            for (Int32 pass = 1; pass <= 2; pass++)
            {
                // Pass 1 just runs all the code without writing out anything
                // to avoid JIT overhead influencing the results
                Time(f1, "DynamicMethod", pass);
                Time(f2, "Lambda", pass);
                Time(f3, "Method", pass);
                Time(f4, "Expression", pass);
            }
        }

        private static void Time(Func<Int32, Int32, Int32> fn,
            String name, Int32 pass)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (Int32 index = 0; index <= 100000000; index++)
            {
                Int32 result = fn(index, 1);
            }
            sw.Stop();
            if (pass == 2)
                Debug.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms");
        }

        private static Int32 Sum(Int32 a, Int32 b)
        {
            return a + b;
        }
    }
}
4

1 に答える 1

54

経由で作成されDynamicMethodたメソッドは2つのサンクを通過しますが、経由で作成されたメソッドはExpression<>いずれも通過しません。

仕組みは次のとおりです。fn(0, 1)メソッドを呼び出すための呼び出しシーケンスはTime次のとおりです(デバッグを容易にするために、引数を0と1にハードコーディングしました)。

00cc032c 6a01            push    1           // 1 argument
00cc032e 8bcf            mov     ecx,edi
00cc0330 33d2            xor     edx,edx     // 0 argument
00cc0332 8b410c          mov     eax,dword ptr [ecx+0Ch]
00cc0335 8b4904          mov     ecx,dword ptr [ecx+4]
00cc0338 ffd0            call    eax // 1 arg on stack, two in edx, ecx

私が調査した最初の呼び出しでは、次のようDynamicMethodcall eax行が表示されます。

00cc0338 ffd0            call    eax {003c2084}
0:000> !u 003c2084
Unmanaged code
003c2084 51              push    ecx
003c2085 8bca            mov     ecx,edx
003c2087 8b542408        mov     edx,dword ptr [esp+8]
003c208b 8b442404        mov     eax,dword ptr [esp+4]
003c208f 89442408        mov     dword ptr [esp+8],eax
003c2093 58              pop     eax
003c2094 83c404          add     esp,4
003c2097 83c010          add     eax,10h
003c209a ff20            jmp     dword ptr [eax]

これは、引数を再配置するためにスタックスウィズリングを実行しているようです。これは、暗黙の「this」引数を使用するデリゲートと使用しないデリゲートの違いによるものと推測します。

最後のジャンプは次のように解決されます。

003c209a ff20            jmp     dword ptr [eax]      ds:0023:012f7edc=0098c098
0098c098 e963403500      jmp     00ce0100

0098c098のコードの残りの部分は、JITサンクのように見えます。その開始はjmpJITの後に書き直されました。実際のコードにたどり着くのは、このジャンプの後でのみです。

0:000> !u eip
Normal JIT generated code
DynamicClass.TestMethod(Int32, Int32)
Begin 00ce0100, size 5
>>> 00ce0100 03ca            add     ecx,edx
00ce0102 8bc1            mov     eax,ecx
00ce0104 c3              ret

を介して作成されたメソッドの呼び出しシーケンスExpression<>は異なります。スタックのスウィズリングコードがありません。これが、最初のジャンプからeax

00cc0338 ffd0            call    eax {00ce00a8}

0:000> !u eip
Normal JIT generated code
DynamicClass.lambda_method(System.Runtime.CompilerServices.ExecutionScope, Int32, Int32)
Begin 00ce00a8, size b
>>> 00ce00a8 8b442404        mov     eax,dword ptr [esp+4]
00ce00ac 03d0            add     edx,eax
00ce00ae 8bc2            mov     eax,edx
00ce00b0 c20400          ret     4

さて、どうやってこんな風になったの?

  1. スタックのスウィズリングは必要ありませんでした(デリゲートからの暗黙の最初の引数が実際に使用されます。つまり、静的メソッドにバインドされたデリゲートとは異なります)
  2. JITは、デリゲートが偽のアドレスではなく実際の宛先アドレスを保持するように、LINQコンパイルロジックによって強制されている必要があります。

LINQがどのようにJITを強制したかはわかりませんが、関数を少なくとも1回呼び出すことによって、自分でJITを強制する方法は知っています。更新:JITを強制する別の方法を見つけました。restrictedSkipVisibilityコンストラクターにargumetnを使用して、を渡しtrueます。したがって、暗黙の「this」パラメーターを使用してスタックのスウィズリングを排除し、代替コンストラクターを使用して、バインドされたアドレスがサンクではなく実際のアドレスになるようにプリコンパイルする変更されたコードを次に示します。

using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;

namespace Sandbox
{
    public class Program
    {
        public static void Main(String[] args)
        {
            DynamicMethod method = new DynamicMethod("TestMethod",
                typeof(Int32), new Type[] { typeof(object), typeof(Int32),
                typeof(Int32) }, true);
            var il = method.GetILGenerator();

            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldarg_2);
            il.Emit(OpCodes.Add);
            il.Emit(OpCodes.Ret);

            Func<Int32, Int32, Int32> f1 =
                (Func<Int32, Int32, Int32>)method.CreateDelegate(
                    typeof(Func<Int32, Int32, Int32>), null);
            Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
            Func<Int32, Int32, Int32> f3 = Sum;
            Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
            Func<Int32, Int32, Int32> f4 = f4x.Compile();
            for (Int32 pass = 1; pass <= 2; pass++)
            {
                // Pass 1 just runs all the code without writing out anything
                // to avoid JIT overhead influencing the results
                Time(f1, "DynamicMethod", pass);
                Time(f2, "Lambda", pass);
                Time(f3, "Method", pass);
                Time(f4, "Expression", pass);
            }
        }

        private static void Time(Func<Int32, Int32, Int32> fn,
            String name, Int32 pass)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (Int32 index = 0; index <= 100000000; index++)
            {
                Int32 result = fn(index, 1);
            }
            sw.Stop();
            if (pass == 2)
                Console.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms");
        }

        private static Int32 Sum(Int32 a, Int32 b)
        {
            return a + b;
        }
    }
}

これが私のシステムのランタイムです:

DynamicMethod: 312 ms
Lambda: 417 ms
Method: 417 ms
Expression: 312 ms

追加するために更新

このコードを新しいシステムで実行してみました。これは、.NET4ベータ2がインストールされたWindows7 x64(mscoree.dll ver。4.0.30902)を実行するCore i7 920であり、結果はさまざまです。

csc 3.5, /platform:x86, runtime v2.0.50727 (via .config)

Run #1
DynamicMethod: 214 ms
Lambda: 571 ms
Method: 570 ms
Expression: 249 ms

Run #2
DynamicMethod: 463 ms
Lambda: 392 ms
Method: 392 ms
Expression: 463 ms

Run #3
DynamicMethod: 463 ms
Lambda: 570 ms
Method: 570 ms
Expression: 463 ms

おそらく、これは結果に影響を与えるIntel SpeedStep、またはおそらくTurboBoostです。いずれにせよ、それは非常に迷惑です。

csc 3.5, /platform:x64, runtime v2.0.50727 (via .config)
DynamicMethod: 428 ms
Lambda: 392 ms
Method: 392 ms
Expression: 428 ms

csc 3.5, /platform:x64, runtime v4
DynamicMethod: 428 ms
Lambda: 356 ms
Method: 356 ms
Expression: 428 ms

csc 4, /platform:x64, runtime v4
DynamicMethod: 428 ms
Lambda: 356 ms
Method: 356 ms
Expression: 428 ms

csc 4, /platform:x86, runtime v4
DynamicMethod: 463 ms
Lambda: 570 ms
Method: 570 ms
Expression: 463 ms

csc 3.5, /platform:x86, runtime v4
DynamicMethod: 214 ms
Lambda: 570 ms
Method: 571 ms
Expression: 249 ms

これらの結果の多くは、C#3.5/ランタイムv2.0シナリオでランダムなスピードアップを引き起こしているものが何であれ、タイミングの偶然です。SpeedStepまたはTurboBoostがこれらの影響の原因であるかどうかを確認するには、再起動する必要があります。

于 2009-08-18T23:17:34.800 に答える