CLR如何在调用c ++函数时避免重击?

时间:2018-08-12 08:47:29

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

MSDN states

  

不管使用哪种互操作技术,每次托管函数调用非托管函数都需要特殊的转换序列,称为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中我们可以看到,重击惩罚增加了400ms。 go3如何摆脱这种惩罚since it calls C++进行实际计算?

即使这个实验由于某种原因是无效的,我的问题仍然存在-每次调用C ++ / Win32时,CLR真的会受到重击吗?

1 个答案:

答案 0 :(得分:8)

基准化是一门妖术,您在这里得到了一些误导性的结果。运行Release版本非常重要,如果您这样做正确,那么您现在会发现go1()不再需要任何时间。本机代码优化器对此有特殊的了解,如果不使用它的结果,它将完全消除它。

您必须更改代码才能获得可靠的结果。首先,在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;
}

请注意,使用__declspec使用sum变量如何迫使优化器保持调用,可以防止整个函数被删除并避免污染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()使用您询问的重击。这些文档对于重排有点太慌张了,所需要做的只是代码在堆栈上写一个cookie,以防止垃圾收集器在寻找对象根时误入非托管堆栈帧。当它还必须转换函数参数和/或返回值时,它 可能会更昂贵,但这不是这种情况。抖动优化器无法消除pow()调用,它不具备CRT功能的专门知识。
  • 尽管测量方法类似,但
  • go3()使用的机制却非常不同。 Math :: Pow()在CLR中是特殊情况,它使用所谓的FCall mechanism。从托管代码到已编译的C ++机器代码,都不会遇到麻烦。这种微优化在CLR / BCL中非常普遍。某种程度上来说,这会产生额外的开销,因为它会对可能引发异常的参数进行检查。这也是抖动优化器没有消除该调用的基本原因,它通常避免使异常消失的优化。