未受管理的x86和x64中的托管互操作性能

Ada*_*dam 3 performance 64-bit interop c++-cli

在我的测试中,我看到在编译x64而不是x86时,非托管到托管互操作的性能成本.导致这种放缓的原因是什么?

我正在测试不在调试器下运行的发布版本.该循环是100,000,000次迭代.

在x86中,我每次互操作平均测量8ns,这似乎与我在其他地方看到的相匹配.Unity的x86互操作是8.2ns.一篇微软文章和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;
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

在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:
Run Code Online (Sandbox Code Playgroud)

FindThunkTarget相当沉重,看起来大部分时间都在那里度过.所以我的工作理论是,在x86中,thunk目标是已知的,执行可以或多或少地直接跳转到它.但是在x64中,thunk目标是未知的,并且在能够跳转到它之前进行搜索过程以找到它.我不知道这是为什么?

Han*_*ant 6

我没有回忆过这样的代码永远保证.7纳秒是您在C++ Interop代码上可以预期的一种性能,托管代码调用本机代码.这反过来说,本机代码调用托管代码,又名"反向pinvoke".

你肯定会得到这种互操作的缓慢味道.就我所见,IJWNOADThunk中的"No AD"是令人讨厌的小细节.这段代码没有得到互操作存根中常见的微优化爱.它也非常特定于C++/CLI代码.令人讨厌,因为它无法假设托管代码需要运行的AppDomain.实际上,它甚至不能假设CLR已加载并初始化.

8ns是我能合理期望的最快速度吗?

是.事实上,这种测量非常低端.你的硬件比我的硬件强很多,我在移动Haswell上进行测试.我看到x86在~26和43纳秒之间,x64在~40到46纳秒之间.所以你获得x3更好的时间,非常令人印象深刻.坦率地说,有点太令人印象深刻,但你看到我所做的相同代码,所以我们必须测量相同的场景.

当x86互操作成本为8ns时,为什么x64互操作成本为17ns?

这不是最佳代码,微软程序员对他可以削减的角落非常悲观.我对这是否有必要没有真正的了解,UMThunkStub.asm中的评论没有解释任何有关选择的内容.

反向pinvoke没有什么特别之处.例如,一个处理Windows消息的GUI程序就会发生这种情况.但这样做完全不同,这样的代码使用委托.哪种方法可以更快地取得成功.使用Marshal :: GetFunctionPointerForDelegate()是关键.我试过这种方法:

using namespace System;
using namespace System::Runtime::InteropServices;


void* GetManagedUpdateFunctionPointer() {
    auto dlg = gcnew Action(&ManagedUpdate);
    auto tobereleased = GCHandle::Alloc(dlg);
    return Marshal::GetFunctionPointerForDelegate(dlg).ToPointer();
}
Run Code Online (Sandbox Code Playgroud)

并在WinMain()函数中使用如下:

typedef void(__stdcall * testfuncPtr)();
testfuncPtr fptr = (testfuncPtr)GetManagedUpdateFunctionPointer();
//Warm stuff up
for (size_t i = 0; i < 100; i++) fptr();

    //...
    for ( size_t i = 0; i < num; i++ ) fptr();
Run Code Online (Sandbox Code Playgroud)

这使得x86版本更快一点.和x64版一样快.

如果要使用此方法,请记住,作为委托目标的实例方法比x64代码中的静态方法更快,调用存根重新排列函数参数的工作量较少.并且要注意我在tobereleased变量上设置了一个快捷方式,这里有一个可能的内存管理细节,并且在插件方案中可能是首选或必需的GCHandle :: Free()调用.