【问题标题】:Why is Calli Faster Than a Delegate Call?为什么 Calli 比代表调用更快?
【发布时间】:2011-05-05 05:22:39
【问题描述】:

我在玩 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 更快...这是我的基准测试的问题,还是有什么我不明白的地方? (顺便说一句,我处于发布模式。)

【问题讨论】:

  • 非常有趣的问题。我也在机器上试过,速度差异很大。我也很想知道其背后的确切原因。
  • 我实际上开始怀疑我的基准测试可能是错误的——中间可能有我没有注意到的说明,这会弄乱结果。不过,现在我想不了太多……

标签: c# .net reflection.emit ilgenerator


【解决方案1】:

鉴于您的性能数据,我假设您必须使用 2.0 框架或类似的东西? 4.0 中的数字要好得多,但“Marshal.GetDelegate”版本仍然较慢。

问题是并非所有代表都是平等的。

托管代码函数的委托本质上只是一个直接的函数调用(在 x86 上,这是一个 __fastcall),如果您正在调用一个静态函数(但这只是 3 或 4 条指令x86)。

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

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

【讨论】:

  • 这家伙知道他在说什么! +1000
【解决方案2】:

很难回答:) 反正我会试试的。

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

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

我的两分钱!

【讨论】:

  • 嗯...我有点困惑:请注意,两个版本都使用委托,只是委托的方式有所不同制作。所以在这方面不应该是相同的。
  • 另外,SuppressUnmanagedCodeSecuritynot 会禁用类型检查安全性,它会执行从托管到非托管转换的堆栈遍历。它用于非托管代码特权;我把它放在那里是为了消除不相关的瓶颈。
  • @mehrdad:你说得对,但速度会受到影响:我们从msdn.microsoft.com/en-us/library/…读到“这个属性可以应用于想要调用本机代码的方法,而不会导致运行的性能损失- 这样做时进行安全检查。”
  • 但如果它在我的代码中禁用了对 all 转换的检查,那为什么会有不同呢?
  • @mehrdad:好点。我不知道。正如我所说,委托编译版本应该有一些更间接的调用(例如,它需要至少一个指针取消引用)。它会更慢,但不会那么慢。谜团还在!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-30
  • 2011-05-01
  • 1970-01-01
  • 2013-03-14
  • 2014-10-12
  • 1970-01-01
相关资源
最近更新 更多