【发布时间】:2017-03-26 08:54:40
【问题描述】:
在我的测试中,我看到在为 x64 而不是 x86 编译时,非托管到托管互操作的性能成本加倍。是什么导致了这种放缓?
我正在测试未在调试器下运行的发布版本。循环是 100,000,000 次迭代。
在 x86 中,我测量每个互操作调用的平均时间为 8ns,这似乎与我在其他地方看到的相匹配。 Unity 的 x86 互操作为 8.2ns。 Microsoft 的一篇文章和 Hans Passant 都提到了 7ns。 8ns 在我的机器上是 28 个时钟周期,这看起来至少是合理的,但我确实想知道是否有可能更快。
在 x64 中,我测量每个互操作调用的平均时间为 17ns。我找不到任何人提到 x86 和 x64 之间的区别,甚至没有提到他们在给出时间时所指的内容。 Unity 的 x64 互操作时钟在 5.9ns 左右。
常规函数调用(包括非托管 C++ DLL)平均花费 1.3ns。这在 x86 和 x64 之间没有显着变化。
下面是我用于测量这个的最小 C++/CLI 代码,尽管在我的实际项目中看到相同的数字,该项目由调用 C++/CLI DLL 的托管端的本机 C++ 项目组成。
#pragma managed
void
ManagedUpdate()
{
}
#pragma unmanaged
#include <wtypes.h>
#include <cstdint>
#include <cwchar>
struct ProfileSample
{
static uint64_t frequency;
uint64_t startTick;
wchar_t* name;
int count;
ProfileSample(wchar_t* name_, int count_)
{
name = name_;
count = count_;
LARGE_INTEGER win32_startTick;
QueryPerformanceCounter(&win32_startTick);
startTick = win32_startTick.QuadPart;
}
~ProfileSample()
{
LARGE_INTEGER win32_endTick;
QueryPerformanceCounter(&win32_endTick);
uint64_t endTick = win32_endTick.QuadPart;
uint64_t deltaTicks = endTick - startTick;
double nanoseconds = (double) deltaTicks / (double) frequency * 1000000000.0 / count;
wchar_t buffer[128];
swprintf(buffer, _countof(buffer), L"%s - %.4f ns\n", name, nanoseconds);
OutputDebugStringW(buffer);
if (!IsDebuggerPresent())
MessageBoxW(nullptr, buffer, nullptr, 0);
}
};
uint64_t ProfileSample::frequency = 0;
int CALLBACK
WinMain(HINSTANCE, HINSTANCE, PSTR, INT)
{
LARGE_INTEGER frequency;
QueryPerformanceFrequency(&frequency);
ProfileSample::frequency = frequency.QuadPart;
//Warm stuff up
for ( size_t i = 0; i < 100; i++ )
ManagedUpdate();
const int num = 100000000;
{
ProfileSample p(L"ManagedUpdate", num);
for ( size_t i = 0; i < num; i++ )
ManagedUpdate();
}
return 0;
}
1) 为什么 x86 互操作成本为 8ns 而 x64 互操作成本为 17ns
2) 8ns 是我可以合理预期的最快速度吗?
编辑 1
附加信息
CPU i7-4770k @ 3.5 GHz
测试用例是 VS2017 中的单个 C++/CLI 项目。
默认发布配置
全面优化/O2
我随机使用了偏好大小或速度、省略帧指针、启用 C++ 异常和安全检查等设置,但似乎都没有改变 x86/x64 差异。
编辑 2
我已经完成了反汇编(此时我还不是很熟悉)。
在 x86 中似乎类似于
call ManagedUpdate
jmp ptr [__mep@?ManagedUpdate@@$$FYAXXZ]
jmp _IJWNOADThunkJumpTarget@0
在 x64 中我看到了
call ManagedUpdate
jmp ptr [__mep@?ManagedUpdate@@$$FYAXXZ]
//Some jumping around that quickly leads to IJWNOADThunk::MakeCall:
call IJWNOADThunk::FindThunkTarget
//MakeCall uses the result from FindThunkTarget to jump into UMThunkStub:
FindThunkTarget 非常繁重,看起来大部分时间都花在了那里。所以我的工作理论是,在 x86 中,thunk 目标是已知的,执行可以或多或少直接跳转到它。但是在 x64 中,thunk 目标是未知的,并且在跳转到它之前会进行搜索过程以找到它。我想知道这是为什么?
【问题讨论】:
-
了解这条性能测试规则:始终启用优化,始终包含您使用的编译设置。在 .NET 世界中,JIT 编译发生在运行时,所以我很高兴您提到您没有使用附加的调试器运行,因为这会抑制 JIT 优化。但是你的编译设置对于本机代码方面也很重要。
-
当您处理绝对数字时,了解硬件很重要。 Xeon 将比 Atom 好得多。
-
我测试了一系列优化设置,但没有什么比这些数字的差异超过 10%。开箱即用的 Visual Studio 2017 Release 配置具有代表性。我会在帖子中添加更多信息。
-
我知道绝对数字会因机器而异,这也是我包含常规 DLL 调用时间的部分原因。如果互操作是 DLL 调用成本的 4 倍是错误的比率,那么有人可能会指出这一点。或者,如果它偏离了一个数量级,那几乎可以肯定是从我所包含的绝对数字中可以观察到的一个问题。问题集中在为什么 x64 的成本是两倍,所以绝对数字远不如它们的相对大小重要。
标签: performance c++-cli 64-bit interop