不管使用哪种互操作技术,每次托管函数调用非托管函数都需要特殊的转换序列,称为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真的会受到重击吗?
答案 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毫秒。
三种工作机制: