0

这个问题与我的另一个问题有关,标题为Calling MASM PROC from C++/CLI in x64 mode 会产生意外的性能问题。我没有收到任何评论和答案,但最终我发现问题是由编译器在托管函数调用非托管函数时插入的函数thunk引起的,反之亦然。我将不再详细介绍,因为今天我不想关注这种隧道机制的另一个后果。

为了为这个问题提供一些上下文,我的问题是为了性能,用 MASM64 文件中的函数替换非托管 C++/CLI 类中的 64 到 128 位无符号整数乘法的 C++ 函数。ASM 替换非常简单:

AsmMul1 proc ; ?AsmMul1@@$$FYAX_K0AEA_K1@Z

; ecx  : Factor1
; edx  : Factor2
; [r8] : ProductL
; [r9] : ProductH

mov  rax, rcx            ; rax = Factor1
mul  rdx                 ; rdx:rax = Factor1 * Factor2
mov  qword ptr [r8], rax ; [r8] = ProductL
mov  qword ptr [r9], rdx ; [r9] = ProductH
ret

AsmMul1 endp

我期望通过使用简单的 CPU MUL 指令将编译函数替换为四个 32 到 64 位乘法来大幅提升性能。最大的惊喜是 ASM 版本比 C++ 版本慢了大约四倍(!)。经过大量的研究和测试,我发现 C++/CLI 中的一些函数调用涉及到 thunking,这显然是一个非常复杂的事情,它比 thunked 函数本身要花费更多的时间。

在阅读了有关此thunking的更多信息后,事实证明,每当您使用编译器选项/clr时,所有函数的调用约定都会默默地更改为__clrcall,这意味着它们成为托管函数。异常是使用编译器内在函数、内联 ASM 以及通过 dllimport 调用其他 DLL 的函数——正如我的测试所揭示的,这似乎包括调用外部 ASM 函数的函数。

只要所有交互函数都使用 __clrcall 约定(即托管),就不会涉及到 thunking,并且一切都运行顺利。一旦在任一方向上越过托管/非托管边界,就会启动thunking,并且性能会严重下降。

现在,在这个冗长的序言之后,让我们进入我的问题的核心。据我了解__clrcall约定和/clr编译器开关,以这种方式在非托管 C++ 类中标记函数会导致编译器发出 MSIL 代码。我在 __clrcall 的文档中找到了这句话:

将函数标记为 __clrcall 时,表明函数实现必须是 MSIL,并且不会生成本机入口点函数。

坦率地说,这吓到我了!毕竟,为了获得真正的本机代码,我正在经历编写 C++/CLI 代码的麻烦,即超快的 x64 机器代码。但是,这似乎不是混合程序集的默认设置。如果我弄错了,请纠正我:如果我使用 VC2017 给出的项目默认值,我的程序集包含 MSIL,它将被 JIT 编译。真的?

有一个#pragma managed似乎抑制了 MSIL 的生成,以支持基于每个功能的本机代码。我已经对其进行了测试,并且它可以工作,但问题是一旦本机代码调用托管函数,thunking 就会再次受到阻碍,反之亦然。在我的 C++/CLI 项目中,我发现无法配置 thunking 和代码生成,而不会在某些地方受到性能影响。

所以我现在问自己:首先使用 C++/CLI 有什么意义?当所有内容仍编译为 MSIL 时,它是否会给我带来性能优势?也许最好用纯 C++ 编写所有内容并使用 Pinvoke 调用这些函数?我不知道,我有点卡在这里。

也许有人可以对这个文档记录非常差的话题有所了解......

4

0 回答 0