在调用c ++函数时,CLR如何避免thunking?

wez*_*ten 2 c# c++ c++-cli

MSDN声明:

无论使用何种互操作技术,每次托管函数调用非托管函数时都需要特殊的转换序列(称为thunks),反之亦然.这些thunk是由Visual C++编译器自动插入的,但重要的是要记住,累积起来,这些转换在性能方面可能很昂贵.

然而,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
Run Code Online (Sandbox Code Playgroud)

结果是(一致地):

405 (go1 - pure C++)
818 (go2 - managed code calling C++)
289 (go3 - pure managed)
Run Code Online (Sandbox Code Playgroud)

为什么go3比go1更快有点神秘,但这不是我的问题.我的问题是,我们从go1&go2看到,thunking惩罚增加了400ms.go3如何摆脱这种惩罚,因为它调用C++来进行实际计算?

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

Han*_*ant 8

基准测试是一种黑色艺术,你在这里得到了一些误导性的结果.运行Release版本非常重要,如果你这样做,那么你现在会注意到go1()不再需要时间了.本机代码优化器具有它的特殊知识,如果你不使用它的结果,那么它完全消除它.

您必须更改代码才能获得可靠的结果.首先在Go()测试体周围放置一个循环,重复至少20次.这消除了jitting和缓存开销,并有助于查看大的标准偏差.敲掉REPS 0,这样你就不用等太久了.赞成工具>选项>调试>常规,"抑制JIT优化"未选中.更改代码,我建议:

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

注意sum变量如何强制优化器保持调用,使用__declspec可以防止删除整个函数并避免污染Go()体.对go2和go3执行相同操作,使用[MethodImpl(MethodImplOptions :: NoInlining)].

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

工作中有三种不同的机制:

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