C++/CLI函数指针与.NET代理的性能

ueb*_*ebe 11 .net performance mixed-mode delegates c++-cli

对于我的C++/CLI项目,我只是尝试测量C++/CLI函数指针与.NET代理的成本.

我的期望是,C++/CLI函数指针比.NET代理更快.所以我的测试分别计算整个5秒内.NET委托和本机函数指针的调用次数.

结果

现在结果对我来说(现在仍然令人震惊):

  • .NET委托: 910M执行,结果为152080413333030,5003ms
  • 函数指针: 347M执行,结果为57893422166551,5013ms

这意味着,本机C++/CLI函数指针的使用率比使用C++/CLI代码中的托管委托慢近3倍.怎么可能?在性能关键部分使用接口,委托或抽象类时,我应该使用托管构造吗?

测试代码

连续调用的函数:

__int64 DoIt(int n, __int64 sum)
{
    if ((n % 3) == 0)
        return sum + n;
    else
        return sum + 1;
}
Run Code Online (Sandbox Code Playgroud)

调用该方法的代码尝试使用所有参数以及返回值,因此没有任何优化(希望如此).这是代码(对于.NET委托):

__int64 executions;
__int64 result;
System::Diagnostics::Stopwatch^ w = gcnew System::Diagnostics::Stopwatch();

System::Func<int, __int64, __int64>^ managedPtr = gcnew System::Func<int, __int64, __int64>(&DoIt);
w->Restart();
executions = 0;
result = 0;
while (w->ElapsedMilliseconds < 5000)
{
    for (int i=0; i < 1000000; i++)
        result += managedPtr(i, executions);
    executions++;
}
System::Console::WriteLine(".NET delegate:       {0}M executions with result {2} in {1}ms", executions, w->ElapsedMilliseconds, result);
Run Code Online (Sandbox Code Playgroud)

与.NET委托调用类似,使用C++函数指针:

typedef __int64 (* DoItMethod)(int n, __int64 sum);

DoItMethod nativePtr = DoIt;
w->Restart();
executions = 0;
result = 0;
while (w->ElapsedMilliseconds < 5000)
{
    for (int i=0; i < 1000000; i++)
        result += nativePtr(i, executions);
    executions++;
}
System::Console::WriteLine("Function pointer:    {0}M executions with result {2} in {1}ms", executions, w->ElapsedMilliseconds, result);
Run Code Online (Sandbox Code Playgroud)

其他信息

  • 使用Visual Studio 2012编译
  • .NET Framework 4.5是针对性的
  • 发布版本(执行计数与Debug版本保持成比例)
  • 调用约定是__stdcall(在使用CLR支持编译项目时不允许__fastcall)

所有测试完成:

  • .NET虚方法:1025M执行,结果为171358304166325,5004ms
  • .NET委托:910M执行,结果为152080413333030,5003ms
  • 虚方法:336M执行,结果56056335999888,5006ms
  • 函数指针:347M执行,结果为57893422166551,5013ms
  • 函数调用:1459M执行,结果为244230520832847,5001ms
  • 内联函数:1385M执行,结果为231791984166205,内存为5000ms

直接调用"DoIt"在这里由"函数调用"表示,它似乎由编译器内联,因为与调用内联函数相比,执行计数没有(显着)差异.

对C++虚拟方法的调用与函数指针一样"慢".托管类(ref类)的虚方法与.NET委托一样快.

更新: 我深入挖掘,似乎对于具有非托管函数的测试,每次调用DoIt函数时都会发生到本机代码的转换.因此,我将内部循环包装到另一个我强制编译为非托管的函数中:

#pragma managed(push, off)
__int64 TestCall(__int64* executions)
{
    __int64 result = 0;
    for (int i=0; i < 1000000; i++)
            result += DoItNative(i, *executions);
    (*executions)++;
    return result;
}
#pragma managed(pop)
Run Code Online (Sandbox Code Playgroud)

另外,我测试了std :: function,如下所示:

#pragma managed(push, off)
__int64 TestStdFunc(__int64* executions)
{
    __int64 result = 0;
    std::function<__int64(int, __int64)> func(DoItNative);
    for (int i=0; i < 1000000; i++)
        result += func(i, *executions);
    (*executions)++;
    return result;
}
#pragma managed(pop)
Run Code Online (Sandbox Code Playgroud)

现在,新的结果是:

  • 函数调用:2946M执行,结果为495340439997054,持续时间为5000ms
  • std :: function:160M执行,结果为26679519999840,5018ms

std :: function有点令人失望.

Han*_*ant 17

你看到"双重thunking"的成本.DoIt()函数的核心问题是它被编译为托管代码.委托调用非常快,通过委托从托管代码转到托管代码并不复杂.但是,函数指针很慢,编译器会自动生成代码,首先从托管代码切换到非托管代码,然后通过函数指针进行调用.然后以存根的形式结束,该存根从非托管代码切换回托管代码并调用DoIt().

据推测,您真正要测量的是对本机代码的调用.使用#pragma强制DoIt()作为机器代码生成,如下所示:

#pragma managed(push, off)
__int64 DoIt(int n, __int64 sum)
{
    if ((n % 3) == 0)
        return sum + n;
    else
        return sum + 1;
}
#pragma managed(pop)
Run Code Online (Sandbox Code Playgroud)

您现在将看到函数指针比委托更快