繁体   English   中英

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

[英]How does the CLR avoid thunking when calling c++ functions?

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

结果是(一致地):

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

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

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

基准测试是一种黑色艺术,你在这里得到了一些误导性的结果。 运行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;
}

注意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中非常常见。 有些必要,因为它对可能引发异常的参数执行检查,所以会产生额外的开销。 此外,抖动优化器没有消除调用的基本原因是,它通常避免了使异常消失的优化。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM