在 x64 模式下从 C++/CLI 调用 MASM PROC 会产生意外的性能问题
Calling MASM PROC from C++/CLI in x64 mode yields unexpected performance problems
我正在编写一个任意精度的整数 class 以用于 C#(64 位)。目前我正在研究乘法例程,使用递归分而治之算法将多位乘法分解为一系列原始的 64 位到 128 位乘法,然后通过简单的方法重新组合其结果添加。为了获得显着的性能提升,我使用本机 x64 C++ 编写代码,嵌入到 C++/CLI 包装器中以使其可从 C# 代码调用。
到目前为止,就算法而言,一切都很好。但是,我的问题是速度优化。由于 64 位到 128 位的乘法是这里真正的瓶颈,我试图在那里优化我的代码。我的第一个简单方法是一个 C++ 函数,它通过执行四个 32 位到 64 位乘法并通过几次移位和加法重新组合结果来实现此乘法。这是源代码:
// 64-bit to 128-bit multiplication, using the following decomposition:
// (a*2^32 + i) (b*2^32 + i) = ab*2^64 + (aj + bi)*2^32 + ij
public: static void Mul (UINT64 u8Factor1,
UINT64 u8Factor2,
UINT64& u8ProductL,
UINT64& u8ProductH)
{
UINT64 u8Result1, u8Result2;
UINT64 u8Factor1L = u8Factor1 & 0xFFFFFFFFULL;
UINT64 u8Factor2L = u8Factor2 & 0xFFFFFFFFULL;
UINT64 u8Factor1H = u8Factor1 >> 32;
UINT64 u8Factor2H = u8Factor2 >> 32;
u8ProductL = u8Factor1L * u8Factor2L;
u8ProductH = u8Factor1H * u8Factor2H;
u8Result1 = u8Factor1L * u8Factor2H;
u8Result2 = u8Factor1H * u8Factor2L;
if (u8Result1 > MAX_UINT64 - u8Result2)
{
u8Result1 += u8Result2;
u8Result2 = (u8Result1 >> 32) | 0x100000000ULL; // add carry
}
else
{
u8Result1 += u8Result2;
u8Result2 = (u8Result1 >> 32);
}
if (u8ProductL > MAX_UINT64 - (u8Result1 <<= 32))
{
u8Result2++;
}
u8ProductL += u8Result1;
u8ProductH += u8Result2;
return;
}
此函数需要两个 64 位值,return需要一个 128 位结果作为两个 64 位量作为参考传递。这很好用。在下一步中,我尝试用调用 CPU 的 MUL 指令的 ASM 代码替换对该函数的调用。由于 x64 模式下不再有内联 ASM,因此必须将代码放入单独的 .asm 文件中。这是实现:
_TEXT segment
; =============================================================================
; multiplication
; -----------------------------------------------------------------------------
; 64-bit to 128-bit multiplication, using the x64 MUL instruction
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
; =============================================================================
_TEXT ends
end
这非常简单明了。使用 extern "C"
前向定义从 C++ 代码引用该函数:
extern "C"
{
void AsmMul1 (UINT64, UINT64, UINT64&, UINT64&);
}
令我惊讶的是,它比 C++ 函数慢得多。为了正确地对性能进行基准测试,我编写了一个 C++ 函数,该函数计算 10,000,000 对伪随机无符号 64 位值并在紧密循环中执行乘法,一个接一个地使用这些实现,具有完全相同的值。代码在发布模式下编译并启用优化。 ASM 版本在循环中花费的时间为 515 毫秒,而 C++ 版本为 125 毫秒(!)。
这很奇怪。于是我在调试器中打开反汇编window,将编译器生成的ASM代码复制过来。这是我在那里找到的,为了可读性和与 MASM 一起使用而略微编辑:
AsmMul3 proc ; ?AsmMul3@@$$FYAX_K0AEA_K1@Z
; ecx : Factor1
; edx : Factor2
; [r8] : ProductL
; [r9] : ProductH
mov eax, 0FFFFFFFFh
and rax, rcx
; UINT64 u8Factor2L = u8Factor2 & 0xFFFFFFFFULL;
mov r10d, 0FFFFFFFFh
and r10, rdx
; UINT64 u8Factor1H = u8Factor1 >> 32;
shr rcx, 20h
; UINT64 u8Factor2H = u8Factor2 >> 32;
shr rdx, 20h
; u8ProductL = u8Factor1L * u8Factor2L;
mov r11, r10
imul r11, rax
mov qword ptr [r8], r11
; u8ProductH = u8Factor1H * u8Factor2H;
mov r11, rdx
imul r11, rcx
mov qword ptr [r9], r11
; u8Result1 = u8Factor1L * u8Factor2H;
imul rax, rdx
; u8Result2 = u8Factor1H * u8Factor2L;
mov rdx, rcx
imul rdx, r10
; if (u8Result1 > MAX_UINT64 - u8Result2)
mov rcx, rdx
neg rcx
dec rcx
cmp rcx, rax
jae label1
; u8Result1 += u8Result2;
add rax, rdx
; u8Result2 = (u8Result1 >> 32) | 0x100000000ULL; // add carry
mov rdx, rax
shr rdx, 20h
mov rcx, 100000000h
or rcx, rdx
jmp label2
; u8Result1 += u8Result2;
label1:
add rax, rdx
; u8Result2 = (u8Result1 >> 32);
mov rcx, rax
shr rcx, 20h
; if (u8ProductL > MAX_UINT64 - (u8Result1 <<= 32))
label2:
shl rax, 20h
mov rdx, qword ptr [r8]
mov r10, rax
neg r10
dec r10
cmp r10, rdx
jae label3
; u8Result2++;
inc rcx
; u8ProductL += u8Result1;
label3:
add rdx, rax
mov qword ptr [r8], rdx
; u8ProductH += u8Result2;
add qword ptr [r9], rcx
ret
AsmMul3 endp
将这段代码复制到我的 MASM 源文件中并从我的基准例程中调用它导致在循环中花费了 547 毫秒。这比 ASM 函数稍慢,但比 C++ 函数慢得多。这更奇怪,因为后者应该执行完全相同的机器代码。
所以我尝试了另一种变体,这次使用手动优化的 ASM 代码,它执行完全相同的四次 32 位到 64 位乘法,但方式更直接。代码应避免跳转和立即值,使用 CPU FLAGS 进行进位评估,并使用指令交错以避免寄存器停顿。这是我想出的:
; 64-bit to 128-bit multiplication, using the following decomposition:
; (a*2^32 + i) (b*2^32 + j) = ab*2^64 + (aj + bi)*2^32 + ij
AsmMul2 proc ; ?AsmMul2@@$$FYAX_K0AEA_K1@Z
; ecx : Factor1
; edx : Factor2
; [r8] : ProductL
; [r9] : ProductH
mov rax, rcx ; rax = Factor1
mov r11, rdx ; r11 = Factor2
shr rax, 32 ; rax = Factor1H
shr r11, 32 ; r11 = Factor2H
and ecx, ecx ; rcx = Factor1L
mov r10d, eax ; r10 = Factor1H
and edx, edx ; rdx = Factor2L
imul rax, r11 ; rax = ab = Factor1H * Factor2H
imul r10, rdx ; r10 = aj = Factor1H * Factor2L
imul r11, rcx ; r11 = bi = Factor1L * Factor2H
imul rdx, rcx ; rdx = ij = Factor1L * Factor2L
xor ecx, ecx ; rcx = 0
add r10, r11 ; r10 = aj + bi
adc ecx, ecx ; rcx = carry (aj + bi)
mov r11, r10 ; r11 = aj + bi
shl rcx, 32 ; rcx = carry (aj + bi) << 32
shl r10, 32 ; r10 = lower (aj + bi) << 32
shr r11, 32 ; r11 = upper (aj + bi) >> 32
add rdx, r10 ; rdx = ij + (lower (aj + bi) << 32)
adc rax, r11 ; rax = ab + (upper (aj + bi) >> 32)
mov qword ptr [r8], rdx ; save ProductL
add rax, rcx ; add carry (aj + bi) << 32
mov qword ptr [r9], rax ; save ProductH
ret
AsmMul2 endp
基准测试产生了 500 毫秒,因此这似乎是这三个 ASM 实现中最快的版本。然而,它们的性能差异很小——但它们都比朴素的 C++ 方法慢四倍!
这是怎么回事?在我看来,从 C++ 调用 ASM 代码通常会降低性能,但我在 Internet 上找不到任何可以解释它的内容。我连接 ASM 的方式正是 Microsoft 推荐的方式。
但是现在,请注意另一件更奇怪的事情!好吧,有编译器内在函数,不是吗? _umul128
内在函数应该完全按照我的 AsmMul1 函数的方式执行,即调用 64 位 CPU MUL 指令。因此,我将 AsmMul1 调用替换为对 _umul128
的相应调用。现在看看我在 return 中获得的性能值(同样,我在一个函数中按顺序 运行 所有四个基准测试):
_umul128: 109 msec
AsmMul2: 94 msec (hand-optimized ASM)
AsmMul3: 125 msec (compiler-generated ASM)
C++ function: 828 msec
现在 ASM 版本速度快得惊人,相对差异与以前大致相同。但是,C++ 函数现在非常懒惰!不知何故,内在函数的使用会颠倒整个性能值。可怕...
对于这种奇怪的行为,我还没有得到任何解释,如果能提供任何关于这里发生的事情的提示,我将不胜感激。如果有人可以解释如何控制这些性能问题,那就更好了。目前我很担心,因为代码中的一个小改动显然会对性能产生巨大影响。我想了解这里的潜在机制,以及如何获得可靠的结果。
还有一件事:为什么 64 到 128 位 MUL 比四个 64 到 64 位 IMUL 慢?!
经过大量的反复试验,并在 Internet 上进行了额外的广泛研究,我似乎找到了这种奇怪的性能行为的原因。神奇的词是 thunking 函数入口点。但让我从头说起。
我的一个观察是,使用哪个编译器内在函数来颠倒我的基准测试结果并不重要。实际上,将 __nop()
(CPU NOP 操作码) 放在函数内的任何位置就足以触发此效果。即使将它放在 return
之前,它也能正常工作。更多测试表明,效果仅限于包含内在函数的函数。 __nop()
对代码流没有任何作用,但显然它改变了包含函数的属性。
我在 Whosebug 上发现了一个似乎解决类似问题的问题:How to best avoid double thunking in C++/CLI native types在评论中,找到了以下附加信息:
One of my own classes in our base library - which uses MFC - is called
about a million times.
We are seeing massive sporadic performance issues, and firing up the
profiler I can see a thunk right at the bottom of this chain. That
thunk takes longer than the method call.
这也正是我所观察到的 - "something" 在函数调用的过程中花费的时间比我的代码长四倍。 __clrcall modifier and in an article about Double Thunking 的文档中对函数 thunk 进行了一定程度的解释。在前者中,有一个使用内在函数的副作用的提示:
You can directly call __clrcall functions from existing C++ code that
was compiled by using /clr as long as that function has an MSIL
implementation. __clrcall functions cannot be called directly from
functions that have inline asm and call CPU-specific intrinisics, for
example, even if those functions are compiled with /clr.
因此,据我所知,包含内在函数的函数会丢失其 __clrcall
修饰符,该修饰符会在指定 /clr 编译器开关时自动添加 - 如果 C++ 函数应该被编译为本机代码。
我没有得到这个 thunking 和 double thunking 的所有细节,但显然需要使非托管函数可从托管函数调用。但是,可以通过将其嵌入到 #pragma managed(push, off)
/ #pragma managed(pop)
对中来关闭每个函数。不幸的是,这个 #pragma 在命名空间块内不起作用,因此可能需要进行一些编辑才能将它放在它应该出现的任何地方。
我已经尝试过这个技巧,将我所有的本机多精度代码放在这个 #pragma 中,并获得了以下基准测试结果:
AsmMul1: 78 msec (64-to-128-bit CPU MUL)
AsmMul2: 94 msec (hand-optimized ASM, 4 x IMUL)
AsmMul3: 125 msec (compiler-generated ASM, 4 x IMUL)
C++ function: 109 msec
现在这看起来很合理,终于!现在所有版本都有大致相同的执行时间,这是我对优化的 C++ 程序的期望。唉,仍然没有幸福的结局……将获胜者 AsmMul1
放入我的多精度乘法器中,所产生的执行时间是没有 #pragma 的 C++ 函数版本的执行时间的两倍。在我看来,解释是这段代码调用了其他 类 中的非托管函数,这些函数在 #pragma 之外,因此具有 __clrcall
修饰符。这似乎又产生了巨大的开销。
坦率地说,我厌倦了进一步调查这个问题。尽管带有单个 MUL 指令的 ASM PROC 似乎击败了所有其他尝试,但收益并没有预期的那么大,而且取消 thunking 会导致我的代码发生如此多的变化,我认为这不值得麻烦。所以我会继续我一开始写的C++函数,本来注定只是一个更好的东西的占位符...
在我看来,C++/CLI 中的 ASM 接口没有得到很好的支持,或者我可能仍然缺少一些基本的东西。也许有一种方法可以让这个函数只对 ASM 函数不产生影响,但到目前为止我还没有找到解决方案。连远程都没有。
请随意在此处添加您自己的想法和观察 - 即使它们只是推测。我认为这仍然是一个非常有趣的话题,需要更多的调查。
我正在编写一个任意精度的整数 class 以用于 C#(64 位)。目前我正在研究乘法例程,使用递归分而治之算法将多位乘法分解为一系列原始的 64 位到 128 位乘法,然后通过简单的方法重新组合其结果添加。为了获得显着的性能提升,我使用本机 x64 C++ 编写代码,嵌入到 C++/CLI 包装器中以使其可从 C# 代码调用。
到目前为止,就算法而言,一切都很好。但是,我的问题是速度优化。由于 64 位到 128 位的乘法是这里真正的瓶颈,我试图在那里优化我的代码。我的第一个简单方法是一个 C++ 函数,它通过执行四个 32 位到 64 位乘法并通过几次移位和加法重新组合结果来实现此乘法。这是源代码:
// 64-bit to 128-bit multiplication, using the following decomposition:
// (a*2^32 + i) (b*2^32 + i) = ab*2^64 + (aj + bi)*2^32 + ij
public: static void Mul (UINT64 u8Factor1,
UINT64 u8Factor2,
UINT64& u8ProductL,
UINT64& u8ProductH)
{
UINT64 u8Result1, u8Result2;
UINT64 u8Factor1L = u8Factor1 & 0xFFFFFFFFULL;
UINT64 u8Factor2L = u8Factor2 & 0xFFFFFFFFULL;
UINT64 u8Factor1H = u8Factor1 >> 32;
UINT64 u8Factor2H = u8Factor2 >> 32;
u8ProductL = u8Factor1L * u8Factor2L;
u8ProductH = u8Factor1H * u8Factor2H;
u8Result1 = u8Factor1L * u8Factor2H;
u8Result2 = u8Factor1H * u8Factor2L;
if (u8Result1 > MAX_UINT64 - u8Result2)
{
u8Result1 += u8Result2;
u8Result2 = (u8Result1 >> 32) | 0x100000000ULL; // add carry
}
else
{
u8Result1 += u8Result2;
u8Result2 = (u8Result1 >> 32);
}
if (u8ProductL > MAX_UINT64 - (u8Result1 <<= 32))
{
u8Result2++;
}
u8ProductL += u8Result1;
u8ProductH += u8Result2;
return;
}
此函数需要两个 64 位值,return需要一个 128 位结果作为两个 64 位量作为参考传递。这很好用。在下一步中,我尝试用调用 CPU 的 MUL 指令的 ASM 代码替换对该函数的调用。由于 x64 模式下不再有内联 ASM,因此必须将代码放入单独的 .asm 文件中。这是实现:
_TEXT segment
; =============================================================================
; multiplication
; -----------------------------------------------------------------------------
; 64-bit to 128-bit multiplication, using the x64 MUL instruction
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
; =============================================================================
_TEXT ends
end
这非常简单明了。使用 extern "C"
前向定义从 C++ 代码引用该函数:
extern "C"
{
void AsmMul1 (UINT64, UINT64, UINT64&, UINT64&);
}
令我惊讶的是,它比 C++ 函数慢得多。为了正确地对性能进行基准测试,我编写了一个 C++ 函数,该函数计算 10,000,000 对伪随机无符号 64 位值并在紧密循环中执行乘法,一个接一个地使用这些实现,具有完全相同的值。代码在发布模式下编译并启用优化。 ASM 版本在循环中花费的时间为 515 毫秒,而 C++ 版本为 125 毫秒(!)。
这很奇怪。于是我在调试器中打开反汇编window,将编译器生成的ASM代码复制过来。这是我在那里找到的,为了可读性和与 MASM 一起使用而略微编辑:
AsmMul3 proc ; ?AsmMul3@@$$FYAX_K0AEA_K1@Z
; ecx : Factor1
; edx : Factor2
; [r8] : ProductL
; [r9] : ProductH
mov eax, 0FFFFFFFFh
and rax, rcx
; UINT64 u8Factor2L = u8Factor2 & 0xFFFFFFFFULL;
mov r10d, 0FFFFFFFFh
and r10, rdx
; UINT64 u8Factor1H = u8Factor1 >> 32;
shr rcx, 20h
; UINT64 u8Factor2H = u8Factor2 >> 32;
shr rdx, 20h
; u8ProductL = u8Factor1L * u8Factor2L;
mov r11, r10
imul r11, rax
mov qword ptr [r8], r11
; u8ProductH = u8Factor1H * u8Factor2H;
mov r11, rdx
imul r11, rcx
mov qword ptr [r9], r11
; u8Result1 = u8Factor1L * u8Factor2H;
imul rax, rdx
; u8Result2 = u8Factor1H * u8Factor2L;
mov rdx, rcx
imul rdx, r10
; if (u8Result1 > MAX_UINT64 - u8Result2)
mov rcx, rdx
neg rcx
dec rcx
cmp rcx, rax
jae label1
; u8Result1 += u8Result2;
add rax, rdx
; u8Result2 = (u8Result1 >> 32) | 0x100000000ULL; // add carry
mov rdx, rax
shr rdx, 20h
mov rcx, 100000000h
or rcx, rdx
jmp label2
; u8Result1 += u8Result2;
label1:
add rax, rdx
; u8Result2 = (u8Result1 >> 32);
mov rcx, rax
shr rcx, 20h
; if (u8ProductL > MAX_UINT64 - (u8Result1 <<= 32))
label2:
shl rax, 20h
mov rdx, qword ptr [r8]
mov r10, rax
neg r10
dec r10
cmp r10, rdx
jae label3
; u8Result2++;
inc rcx
; u8ProductL += u8Result1;
label3:
add rdx, rax
mov qword ptr [r8], rdx
; u8ProductH += u8Result2;
add qword ptr [r9], rcx
ret
AsmMul3 endp
将这段代码复制到我的 MASM 源文件中并从我的基准例程中调用它导致在循环中花费了 547 毫秒。这比 ASM 函数稍慢,但比 C++ 函数慢得多。这更奇怪,因为后者应该执行完全相同的机器代码。
所以我尝试了另一种变体,这次使用手动优化的 ASM 代码,它执行完全相同的四次 32 位到 64 位乘法,但方式更直接。代码应避免跳转和立即值,使用 CPU FLAGS 进行进位评估,并使用指令交错以避免寄存器停顿。这是我想出的:
; 64-bit to 128-bit multiplication, using the following decomposition:
; (a*2^32 + i) (b*2^32 + j) = ab*2^64 + (aj + bi)*2^32 + ij
AsmMul2 proc ; ?AsmMul2@@$$FYAX_K0AEA_K1@Z
; ecx : Factor1
; edx : Factor2
; [r8] : ProductL
; [r9] : ProductH
mov rax, rcx ; rax = Factor1
mov r11, rdx ; r11 = Factor2
shr rax, 32 ; rax = Factor1H
shr r11, 32 ; r11 = Factor2H
and ecx, ecx ; rcx = Factor1L
mov r10d, eax ; r10 = Factor1H
and edx, edx ; rdx = Factor2L
imul rax, r11 ; rax = ab = Factor1H * Factor2H
imul r10, rdx ; r10 = aj = Factor1H * Factor2L
imul r11, rcx ; r11 = bi = Factor1L * Factor2H
imul rdx, rcx ; rdx = ij = Factor1L * Factor2L
xor ecx, ecx ; rcx = 0
add r10, r11 ; r10 = aj + bi
adc ecx, ecx ; rcx = carry (aj + bi)
mov r11, r10 ; r11 = aj + bi
shl rcx, 32 ; rcx = carry (aj + bi) << 32
shl r10, 32 ; r10 = lower (aj + bi) << 32
shr r11, 32 ; r11 = upper (aj + bi) >> 32
add rdx, r10 ; rdx = ij + (lower (aj + bi) << 32)
adc rax, r11 ; rax = ab + (upper (aj + bi) >> 32)
mov qword ptr [r8], rdx ; save ProductL
add rax, rcx ; add carry (aj + bi) << 32
mov qword ptr [r9], rax ; save ProductH
ret
AsmMul2 endp
基准测试产生了 500 毫秒,因此这似乎是这三个 ASM 实现中最快的版本。然而,它们的性能差异很小——但它们都比朴素的 C++ 方法慢四倍!
这是怎么回事?在我看来,从 C++ 调用 ASM 代码通常会降低性能,但我在 Internet 上找不到任何可以解释它的内容。我连接 ASM 的方式正是 Microsoft 推荐的方式。
但是现在,请注意另一件更奇怪的事情!好吧,有编译器内在函数,不是吗? _umul128
内在函数应该完全按照我的 AsmMul1 函数的方式执行,即调用 64 位 CPU MUL 指令。因此,我将 AsmMul1 调用替换为对 _umul128
的相应调用。现在看看我在 return 中获得的性能值(同样,我在一个函数中按顺序 运行 所有四个基准测试):
_umul128: 109 msec
AsmMul2: 94 msec (hand-optimized ASM)
AsmMul3: 125 msec (compiler-generated ASM)
C++ function: 828 msec
现在 ASM 版本速度快得惊人,相对差异与以前大致相同。但是,C++ 函数现在非常懒惰!不知何故,内在函数的使用会颠倒整个性能值。可怕...
对于这种奇怪的行为,我还没有得到任何解释,如果能提供任何关于这里发生的事情的提示,我将不胜感激。如果有人可以解释如何控制这些性能问题,那就更好了。目前我很担心,因为代码中的一个小改动显然会对性能产生巨大影响。我想了解这里的潜在机制,以及如何获得可靠的结果。
还有一件事:为什么 64 到 128 位 MUL 比四个 64 到 64 位 IMUL 慢?!
经过大量的反复试验,并在 Internet 上进行了额外的广泛研究,我似乎找到了这种奇怪的性能行为的原因。神奇的词是 thunking 函数入口点。但让我从头说起。
我的一个观察是,使用哪个编译器内在函数来颠倒我的基准测试结果并不重要。实际上,将 __nop()
(CPU NOP 操作码) 放在函数内的任何位置就足以触发此效果。即使将它放在 return
之前,它也能正常工作。更多测试表明,效果仅限于包含内在函数的函数。 __nop()
对代码流没有任何作用,但显然它改变了包含函数的属性。
我在 Whosebug 上发现了一个似乎解决类似问题的问题:How to best avoid double thunking in C++/CLI native types在评论中,找到了以下附加信息:
One of my own classes in our base library - which uses MFC - is called about a million times. We are seeing massive sporadic performance issues, and firing up the profiler I can see a thunk right at the bottom of this chain. That thunk takes longer than the method call.
这也正是我所观察到的 - "something" 在函数调用的过程中花费的时间比我的代码长四倍。 __clrcall modifier and in an article about Double Thunking 的文档中对函数 thunk 进行了一定程度的解释。在前者中,有一个使用内在函数的副作用的提示:
You can directly call __clrcall functions from existing C++ code that was compiled by using /clr as long as that function has an MSIL implementation. __clrcall functions cannot be called directly from functions that have inline asm and call CPU-specific intrinisics, for example, even if those functions are compiled with /clr.
因此,据我所知,包含内在函数的函数会丢失其 __clrcall
修饰符,该修饰符会在指定 /clr 编译器开关时自动添加 - 如果 C++ 函数应该被编译为本机代码。
我没有得到这个 thunking 和 double thunking 的所有细节,但显然需要使非托管函数可从托管函数调用。但是,可以通过将其嵌入到 #pragma managed(push, off)
/ #pragma managed(pop)
对中来关闭每个函数。不幸的是,这个 #pragma 在命名空间块内不起作用,因此可能需要进行一些编辑才能将它放在它应该出现的任何地方。
我已经尝试过这个技巧,将我所有的本机多精度代码放在这个 #pragma 中,并获得了以下基准测试结果:
AsmMul1: 78 msec (64-to-128-bit CPU MUL)
AsmMul2: 94 msec (hand-optimized ASM, 4 x IMUL)
AsmMul3: 125 msec (compiler-generated ASM, 4 x IMUL)
C++ function: 109 msec
现在这看起来很合理,终于!现在所有版本都有大致相同的执行时间,这是我对优化的 C++ 程序的期望。唉,仍然没有幸福的结局……将获胜者 AsmMul1
放入我的多精度乘法器中,所产生的执行时间是没有 #pragma 的 C++ 函数版本的执行时间的两倍。在我看来,解释是这段代码调用了其他 类 中的非托管函数,这些函数在 #pragma 之外,因此具有 __clrcall
修饰符。这似乎又产生了巨大的开销。
坦率地说,我厌倦了进一步调查这个问题。尽管带有单个 MUL 指令的 ASM PROC 似乎击败了所有其他尝试,但收益并没有预期的那么大,而且取消 thunking 会导致我的代码发生如此多的变化,我认为这不值得麻烦。所以我会继续我一开始写的C++函数,本来注定只是一个更好的东西的占位符...
在我看来,C++/CLI 中的 ASM 接口没有得到很好的支持,或者我可能仍然缺少一些基本的东西。也许有一种方法可以让这个函数只对 ASM 函数不产生影响,但到目前为止我还没有找到解决方案。连远程都没有。
请随意在此处添加您自己的想法和观察 - 即使它们只是推测。我认为这仍然是一个非常有趣的话题,需要更多的调查。