Performance x86和x64中的非托管到托管互操作性能

Performance x86和x64中的非托管到托管互操作性能,performance,c++-cli,64-bit,interop,Performance,C++ Cli,64 Bit,Interop,在我的测试中,我看到在为x64而不是x86编译时,非托管到托管互操作的性能成本翻了一番。是什么导致了这种减速 我正在测试未在调试器下运行的发布版本。循环是100000000次迭代 在x86中,我测量每个互操作调用的平均值为8ns,这似乎与我在其他地方看到的一致。Unity的x86互操作是8.2ns。微软的一篇文章和Hans Passant都提到了7ns。在我的机器上,8ns是28个时钟周期,这似乎至少是合理的,尽管我确实想知道是否有可能走得更快 在x64中,我测量每个互操作调用的平均值为17ns

在我的测试中,我看到在为x64而不是x86编译时,非托管到托管互操作的性能成本翻了一番。是什么导致了这种减速

我正在测试未在调试器下运行的发布版本。循环是100000000次迭代

在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++项目调用C++的c++dll的管理端组成。

#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;
}
在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:

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

我不记得曾经对这样的代码提供过性能保证。7纳秒是C++上互操作代码、托管代码调用本机代码时所能想到的PYF。这与此相反,本机代码调用托管代码,也称为“反向pinvoke”

您肯定会感受到这种互操作的缓慢味道。据我所知,IJWNOADThunk中的“无广告”是一个令人讨厌的小细节。这段代码没有得到互操作存根中常见的微优化。它也非常特定于C++/CLI代码。因为它不能假设托管代码需要在其中运行的AppDomain的任何内容。事实上,它甚至不能假设CLR已加载并初始化

8ns是我可以合理预期的最快速度吗

对。事实上,你的测量值非常低。你的硬件比我的强多了,我正在移动Haswell上测试。我看到x86在26到43纳秒之间,x64在40到46纳秒之间。因此,您的x3表现更好,令人印象深刻。坦率地说,有点太令人印象深刻了,但您看到的代码与我看到的代码相同,因此我们必须测量相同的场景

为什么x64互操作成本为17ns,而x86互操作成本为8ns

这不是最优的代码,微软的程序员对他能走的弯路非常悲观。我不知道这是否有道理,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();
}
在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();
typedef void(u stdcall*testfuncPtr)();
testfuncPtr fptr=(testfuncPtr)GetManagedUpdateFunctionPointer();
//热身
对于(尺寸i=0;i<100;i++)fptr();
//...
对于(size_t i=0;i
这使得x86版本的速度加快了一点。和x64版本一样快


如果要使用这种方法,请记住作为委托目标的实例方法比x64代码中的静态方法快,那么调用存根在重新排列函数参数方面所做的工作更少。请注意,我在
toberelease
变量上选择了一个快捷方式,这里可能有一个内存管理细节,在插件场景中,GCHandle::Free()调用可能是首选或必需的。

了解性能测试的这条规则:始终启用优化,始终包括您使用的编译设置。在.NET世界中,JIT编译是在运行时进行的,所以我很高兴您提到您没有使用附加的调试器运行,因为这会抑制JIT优化。但是你的编译设置对于本机代码来说也很重要。当你处理绝对数时,了解硬件很重要。一个Xeon将比一个Atom有更好的转变。我测试了一系列的优化设置,没有什么比这些数字的变化更大的了。Visual Studio 2017发行版的开箱即用配置具有代表性。我会在这篇文章中添加更多的信息。我知道每台机器的绝对数字会有所不同,这也是我加入常规DLL调用次数的部分原因。如果互操作是DLL调用成本的4倍,那么有人可能会指出这个比例是错误的。或者如果它偏离了一个数量级,这几乎可以肯定是一个从我包含的绝对数字中可以观察到的问题。问题的核心是为什么x64的价格是它的两倍,所以绝对数字远不如相对大小重要。谢谢,这正是我想要的信息。今晚我将尝试一下委托方法并进行比较。对不起,我不是想暗示你做了任何性能保证。我指的是这个帖子,您提到7NS对于托管非托管C++互操作是合理的。我知道这有点像苹果对桔子,但我认为相反的方向会有类似的性能。这主要是一个数量级的理智检查。真正令人好奇的是x86和x64之间的差异。
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();