无论使用何种互操作技术,每次托管函数调用非托管函数时都需要特殊的转换序列(称为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惩罚?
基准测试是一种黑色艺术,你在这里得到了一些误导性的结果.运行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毫秒.
工作中有三种不同的机制: