为什么Calli比委托调用快

本文关键字:调用 Calli 为什么 | 更新日期: 2023-09-27 17:54:42

我正在玩反射。并发现了很少使用的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 version: 77
  • P/Invoke version: 31

显然,p/Invoke更快…这是我的基准测试有问题,还是有什么我不明白的地方?(顺便说一下,我处于释放模式)

为什么Calli比委托调用快

考虑到您的性能数字,我认为您一定在使用2.0框架,或者类似的东西?4.0版本的数据好多了,但是"元帅"。版本仍然较慢。

问题是并不是所有的委托都是平等的。

托管代码函数的委托本质上只是一个直接的函数调用(在x86上,这是__fastcall),如果你调用静态函数(但在x86上只有3或4条指令),则添加了一点"switcheroo"。

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

我通常发现,在不认识。net开发团队的人的情况下,要想弄清楚/托管/非托管互操作发生了什么,最好的办法是用WinDbg和SOS做一些挖掘。

很难回答:)不管怎样,我要试试。

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

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

我的两分钱!