为什么Calli比委托调用更快?

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 - Calli版本:46
64位: - 委托版本:326 - Calli版本:83
现在问题显而易见,为什么速度差别如此之大呢?
更新: 我也创建了一个64位的P/Invoke版本:
- 委托版本:284 - Calli版本:77 - P/Invoke版本:31
显然,P/Invoke更快... 这是我的基准测试有问题,还是有我不理解的东西?(顺便说一句,我处于发布模式。)

非常有趣的问题。我也尝试在机器上运行,发现速度差异很大。我也很想知道背后的确切原因。 - AbhiRoczz...
我开始怀疑我的基准测试可能有误 - 可能有一些我没有注意到的中间指令会搞乱结果。不过现在我想不出还有什么其他问题了... - user541686
2个回答

12
考虑到你的性能数字,我猜你可能正在使用2.0框架或类似的东西? 数字在4.0中要好得多,但“Marshal.GetDelegate”版本仍然较慢。
问题在于并非所有委托都是平等的。
托管代码函数的委托本质上只是一个直接的函数调用(在x86上,那是__fastcall),如果您调用静态函数,则还添加了一个小的“switcheroo”(但在x86上只有3或4条指令)。
另一方面,“Marshal.GetDelegateForFunctionPointer”创建的委托是对一个“存根”函数的直接调用,该函数在调用非托管函数之前会进行一些开销(包括缓冲区和其他操作)。 在这种情况下,缓冲区很少,并且在4.0中此调用的缓冲区似乎已经优化了(但在2.0上可能仍通过ML解释器进行)-但即使在4.0中,堆栈行走也需要非托管代码权限,而不是您的calli委托的一部分。
一般来说,除非认识.NET dev团队中的某个人,否则您在弄清楚托管/非托管互操作方面发生了什么方面最好是要用WinDbg和SOS进行一些挖掘。

1
这个人知道他在说什么!+1000 - Brans Ds

6

很难回答 :)

无论如何我都会尝试。

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

委托版本将具有一些编译代码来检查类型,并且还将执行解引用调用(因为委托类似于类型化函数指针)。

这是我的个人意见!


嗯...我有点困惑:请注意,两个版本都使用了委托,只是在如何创建委托方面存在差异。因此,在这方面两者不应该是相同的吗? - user541686
此外,SuppressUnmanagedCodeSecurity并不会禁用类型检查安全性,并且它会在从托管到非托管的转换中进行堆栈遍历。它是为了非托管代码特权而存在的;我将其放在那里是为了消除无关的瓶颈。 - user541686
@mehrdad:你是对的,但速度会受影响:我们从http://msdn.microsoft.com/en-us/library/system.security.suppressunmanagedcodesecurityattribute.aspx读取到:“此特性可应用于希望调用本机代码而不产生运行时安全检查性能损失的方法。” - daitangio
但是如果它禁用了我的代码中所有转换的检查,那么它为什么会有所不同呢? - user541686
@mehrdad:好观点。我不知道。正如我所说,委托编译版本应该有更多的间接调用(例如,它至少需要一个指针解引用)。它会慢一些,但不会太慢。谜团仍在这里! - daitangio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接