代码之家  ›  专栏  ›  技术社区  ›  wezten

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

  •  2
  • wezten  · 技术社区  · 6 年前

    MSDN states :

    但是CLR确实调用C++&Win32函数。为了处理文件/network/windows和几乎所有其他文件,必须调用非托管代码。它是如何摆脱分块处罚的?

    #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看到重击的惩罚增加了400毫秒。go3是如何摆脱这个惩罚的, since it calls C++ 做实际计算?

    即使这个实验是出于某种原因无效,我的问题仍然是:CLR每次调用C++/Win32真的有一个Tun处罚吗?

    1 回复  |  直到 6 年前
        1
  •  8
  •   Hans Passant    6 年前

    基准测试是一门黑色艺术,你在这里得到了一些误导性的结果。运行版本构建非常重要,如果您做得对,那么您现在会注意到go1()不再需要任何时间。本机代码优化器对此有专门的知识,如果不使用它的结果,那么它将完全消除它。

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

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

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

    三种不同的工作机制:

    • go2()使用您询问的thunk。文档对于重击有点过于恐慌,只需要代码在堆栈上写一个cookie,以防止垃圾收集器在查找对象根时出错进入非托管堆栈帧。它 可以 如果还需要转换函数参数和/或返回值,则代价会更高,但这里不是这样。抖动优化器不能消除pow()调用,它对CRT函数没有特殊的知识。
    • go3()使用一种非常不同的机制,尽管有类似的度量。Pow()是CLR中的特殊情况,它使用所谓的 FCall mechanism