c# - CLR 在调用 C++ 函数时如何避免 thunking?

标签 c# c++ c++-cli

MSDN states :

Regardless of the interop technique used, special transition sequences, called thunks, are required each time a managed function calls an unmanaged function and vice versa. These thunks are inserted automatically by the Visual C++ compiler, but it is important to keep in mind that cumulatively, these transitions can be expensive in terms of performance.

然而,CLR 肯定会一直调用 C++ 和 Win32 函数。为了处理文件/网络/窗口和几乎任何其他东西,必须调用非托管代码。它是如何摆脱分块惩罚的?

这是一个用 C++/CLI 编写的实验,可能有助于描述我的问题:

#define REPS 10000000

#pragma unmanaged
void go1() {
    for (int i = 0; i < REPS; i++)
        pow(i, 3);
}
#pragma managed
void go2() {
    for (int i = 0; i < REPS; i++)
        pow(i, 3);
}
void go3() {
    for (int i = 0; i < REPS; i++)
        Math::Pow(i, 3);
}

public ref class C1 {
public:
    static void Go() {
        auto sw = Stopwatch::StartNew();
        go1();
        Console::WriteLine(sw->ElapsedMilliseconds);
        sw->Restart();
        go2();
        Console::WriteLine(sw->ElapsedMilliseconds);
        sw->Restart();
        go3();
        Console::WriteLine(sw->ElapsedMilliseconds);
    }
};

//Go is called from a C# app

结果(始终如一):

405 (go1 - pure C++)
818 (go2 - managed code calling C++)
289 (go3 - pure managed)

为什么 go3 比 go1 快有点神秘,但这不是我的问题。我的问题是我们从 go1 和 go2 看到 thunking 惩罚增加了 400 毫秒。 go3 是如何摆脱这种惩罚的,since it calls C++进行实际计算?

即使这个实验由于某种原因无效,我的问题仍然存在 - CLR 每次调用 C++/Win32 时真的有一个 thunking 惩罚吗?

最佳答案

基准测试是一门魔法,你在这里得到了一些误导性的结果。运行发布版本非常重要,如果你做对了那么你现在会注意到 go1() 不再需要任何时间。 native 代码优化器对此有特殊的了解,如果您不使用它的结果,它就会完全消除它。

您必须更改代码才能获得可靠的结果。首先在 Go() 测试体周围放一个循环,至少重复 20 次。这消除了抖动和缓存开销,并有助于查看大的标准偏差。将 REPS 设为 0,这样您就不必等待太久。支持工具 > 选项 > 调试 > 常规,取消选中“抑制 JIT 优化”。更改代码,我建议:

__declspec(noinline)
double go1() {
    double sum = 0;
    for (int i = 0; i < REPS; i++)
        sum += pow(i, 3);
    return sum;
}

请注意 sum 变量如何强制优化器保持调用,使用 __declspec 可防止删除整个函数并避免污染 Go() 主体。对 go2 和 go3 做同样的事情,使用 [MethodImpl(MethodImplOptions::NoInlining)]。

我在笔记本电脑上看到的结果:x64:75、84、84,x86:73、89、89 +5/-3 毫秒。

三种不同的机制在起作用:

  • go1() 代码生成与您在 native 代码中所期望的一样,是在 x64 模式下直接调用 __libm_sse2_pow_precise() CRT 函数。除了在发布版本中删除它的风险外,这里没有什么特别的。
  • go2() 使用您询问的 thunk。文档对 thunk 有点太 panic 了,代码所需要的只是在堆栈上写入一个 cookie,以防止垃圾收集器在查找对象根时误入非托管堆栈帧。当它还必须转换函数参数和/或返回值时,它可能会更昂贵,但这里不是这种情况。抖动优化器无法消除 pow() 调用,它对 CRT 函数没有特殊了解。
  • go3() 使用了一种非常不同的机制,尽管测量相似。 Math::Pow() 在 CLR 中是特殊的,它使用所谓的 FCall mechanism .没有 thunk,直接从托管代码到编译的 C++ 机器代码。这种微优化在 CLR/BCL 中很常见。有点必要,因为它对可能引发异常的参数执行检查,所以会有额外的开销。这也是抖动优化器没有消除调用的基本原因,它通常会避免使异常消失的优化。

关于c# - CLR 在调用 C++ 函数时如何避免 thunking?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51807211/

相关文章:

c++ - pthreads 的 Makefile

c# - 如何将 Bitmap 对象转换为 Mat 对象(opencv)?

c++ - 将 System::String 转换为 std::string 时,std::string delete 运算符出现运行时异常

c# - Linq to Entities - where 语句抛出 System.NotSupported 异常

c# - bool Prop 匹配

c# - klout api 总是返回未授权

c++ - (重定义错误)在C++中从一个基类创建多个继承类

c++ - 查找 vector 构造的交点

C# WPF 线程

c++ - 无法在表单文本框中更新(或连续写入)计数器