45

Reflection.Emit をいじっていて、あまり使われていないEmitCalli. 興味をそそられて、通常のメソッド呼び出しと何か違うのだろうかと思ったので、以下のコードを作成しました。

using System;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Security;

[SuppressUnmanagedCodeSecurity]
static class Program
{
    const long COUNT = 1 << 22;
    static readonly byte[] multiply = IntPtr.Size == sizeof(int) ?
      new byte[] { 0x8B, 0x44, 0x24, 0x04, 0x0F, 0xAF, 0x44, 0x24, 0x08, 0xC3 }
    : new byte[] { 0x0f, 0xaf, 0xca, 0x8b, 0xc1, 0xc3 };

    static void Main()
    {
        var handle = GCHandle.Alloc(multiply, GCHandleType.Pinned);
        try
        {
            //Make the native method executable
            uint old;
            VirtualProtect(handle.AddrOfPinnedObject(),
                (IntPtr)multiply.Length, 0x40, out old);
            var mulDelegate = (BinaryOp)Marshal.GetDelegateForFunctionPointer(
                handle.AddrOfPinnedObject(), typeof(BinaryOp));

            var T = typeof(uint); //To avoid redundant typing

            //Generate the method
            var method = new DynamicMethod("Mul", T,
                new Type[] { T, T }, T.Module);
            var gen = method.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Ldc_I8, (long)handle.AddrOfPinnedObject());
            gen.Emit(OpCodes.Conv_I);
            gen.EmitCalli(OpCodes.Calli, CallingConvention.StdCall,
                T, new Type[] { T, T });
            gen.Emit(OpCodes.Ret);

            var mulCalli = (BinaryOp)method.CreateDelegate(typeof(BinaryOp));

            var sw = Stopwatch.StartNew();
            for (int i = 0; i < COUNT; i++) { mulDelegate(2, 3); }
            Console.WriteLine("Delegate: {0:N0}", sw.ElapsedMilliseconds);
            sw.Reset();

            sw.Start();
            for (int i = 0; i < COUNT; i++) { mulCalli(2, 3); }
            Console.WriteLine("Calli:    {0:N0}", sw.ElapsedMilliseconds);
        }
        finally { handle.Free(); }
    }

    delegate uint BinaryOp(uint a, uint b);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool VirtualProtect(
        IntPtr address, IntPtr size, uint protect, out uint oldProtect);
}

コードを x86 モードと x64 モードで実行しました。結果?

32 ビット:

  • デリゲート バージョン: 994
  • カリのバージョン: 46

64 ビット:

  • デリゲート バージョン: 326
  • カリのバージョン: 83

質問はもう明らかだと思います...なぜこんなに大きな速度差があるのでしょうか?


アップデート:

64 ビットの P/Invoke バージョンも作成しました。

  • デリゲート バージョン: 284
  • カリのバージョン: 77
  • P/Invoke バージョン: 31

どうやら、P/Invoke の方が速いようです...これは私のベンチマークの問題ですか、それとも私が理解できない何かが起こっているのでしょうか? (ちなみに私はリリースモードです。)

4

2 に答える 2

11

パフォーマンスの数値を考えると、2.0 フレームワークまたは類似のものを使用しているに違いないと思いますか? 数値は 4.0 の方がはるかに優れていますが、「Marshal.GetDelegate」バージョンはまだ低速です。

問題は、すべてのデリゲートが同じように作られているわけではないということです。

マネージ コード関数のデリゲートは、本質的に単純な関数呼び出し (x86 では __fastcall です) であり、静的関数を呼び出す場合は少し "switcheroo" が追加されます (ただし、x86 では 3 つまたは 4 つの命令です)。

一方、「Marshal.GetDelegateForFunctionPointer」によって作成されたデリゲートは、アンマネージ関数を呼び出す前に少しのオーバーヘッド (マーシャリングなど) を行う「スタブ」関数への直接的な関数呼び出しです。この場合、マーシャリングはほとんどなく、この呼び出しのマーシャリングは 4.0 でかなり最適化されているように見えます (ただし、2.0 ではまだ ML インタープリターを経由する可能性が最も高いです)。 calliデリゲートの一部ではありません。

私は一般的に、.NET 開発チームの誰かを知らない限り、マネージド/アンマネージド相互運用で何が起こっているのかを理解するための最善の策は、WinDbg と SOS を少し掘り下げることだと思います。

于 2012-02-13T14:11:19.670 に答える
6

答えにくいです :) とにかくやってみます。

EmitCalli は生のバイト コード呼び出しであるため、高速です。SuppressUnmanagedCodeSecurity は、スタック オーバーランや範囲外のインデックス チェックなど、一部のチェックも無効にすると思われます。したがって、コードは安全ではなく、全速力で実行されます。

デリゲート バージョンには、型指定をチェックするコンパイル済みコードが含まれており、逆参照呼び出しも行います (デリゲートは型付き関数ポインターに似ているため)。

私の2セント!

于 2011-05-05T07:52:14.543 に答える