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),如果您正在调用一个静态函数(但在 x86 上只有 3 或 4 条指令),则会添加一点“switcheroo”。

另一方面,由“Marshal.GetDelegateForFunctionPointer”创建的委托是对“存根”函数的直接函数调用,它在调用非托管函数之前会做一些开销(编组和诸如此类)。在这种情况下,编组非常少,并且此调用的编组似乎在 4.0 中得到了优化(但很可能仍然通过 2.0 上的 ML 解释器) - 但即使在 4.0 中,也有一个 stackWalk 要求非托管代码权限不是您的 calli 代表的一部分。

我通常发现,如果不认识 .NET 开发团队中的某个人,那么要想弄清楚托管/非托管互操作发生了什么,最好的办法就是对 WinDbg 和 SOS 进行一些挖掘。

于 2012-02-13T14:11:19.670 回答
6

很难回答:) 无论如何我会尝试的。

EmitCalli 更快,因为它是一个原始字节码调用。我怀疑 SuppressUnmanagedCodeSecurity 也会禁用一些检查,例如堆栈溢出/数组越界索引检查。所以代码不安全,全速运行。

委托版本将有一些编译代码来检查类型,并且还会进行取消引用调用(因为委托就像一个类型函数指针)。

我的两分钱!

于 2011-05-05T07:52:14.543 回答