首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在x64模式下从C++/CLI调用MASM会产生意想不到的性能问题

在x64模式下从C++/CLI调用MASM会产生意想不到的性能问题
EN

Stack Overflow用户
提问于 2019-03-20 17:03:35
回答 1查看 235关注 0票数 1

我正在编写一个用于C# (64位)的任意精度整数类。目前我正在研究乘法例程,使用递归的分而治之算法将多位乘法分解成一系列原始的64到128位乘法,其结果通过简单的加法重新组合。为了获得显著的性能提升,我正在编写本机x64 C++中的代码,该代码嵌入到C++/CLI包装器中,使其可以从C#代码中调用。

到目前为止,对于算法来说,这一切都很好。然而,我的问题是速度的优化。由于64到128位乘法是这里真正的瓶颈,我试图在那里优化我的代码。我的第一个简单的方法是一个C++函数,它通过执行四个32到64位的乘法来实现这个乘法,并将结果与几个移位和加法重新组合。这是源代码:

代码语言:javascript
复制
// 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位值,并返回128位结果,作为两个64位值作为引用传递。这个很好用。在接下来的步骤中,我尝试用调用CPU的MUL指令的ASM代码替换对这个函数的调用。由于在x64模式下不再存在内联.asm,所以必须将代码放入一个单独的.asm文件中。这就是执行情况:

代码语言:javascript
复制
_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

这是最简单和最直接的。该函数使用C++转发定义从extern "C"代码中引用:

代码语言:javascript
复制
extern "C"
    {
    void AsmMul1 (UINT64, UINT64, UINT64&, UINT64&);
    }

令我惊讶的是,它被证明比C++函数慢得多。为了正确地对性能进行基准测试,我编写了一个C++函数,它计算10,000,000对伪随机的无符号64位值,并在一个紧循环中执行乘法,使用这些实现一个接一个地使用完全相同的值。代码是在打开优化的情况下以发布模式编译的。ASM版本在循环中花费的时间为515 msec,而ASM版本为125 msec (!)对于C++版本。

太奇怪了。因此,我在调试器中打开了反汇编窗口,并复制了编译器生成的ASM代码。这就是我在那里发现的,为了可读性和与MASM一起使用,略有编辑:

代码语言:javascript
复制
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 msec。这比ASM函数稍慢,也比C++函数慢得多。这就更奇怪了,因为后者应该执行完全相同的机器代码。

因此,我尝试了另一个变体,这次使用手工优化的ASM代码,完全相同的四个32到64位的乘法,但以一种更直接的方式。代码应避免跳转和直接值,使用CPU标志进行进位评估,并使用指令交织以避免注册中断。这就是我想出来的:

代码语言:javascript
复制
; 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代码有一些一般的性能损失,但我在互联网上找不到任何可能解释这一点的东西。我连接ASM的方式正是微软推荐的方式。

但是现在,注意另一件更奇怪的事情!嗯,有编译器的本质,对不对?据推测,_umul128内部应该做我的AsmMul1函数所做的事情,即调用64位CPU MUL指令。因此,我将AsmMul1调用替换为对_umul128的相应调用。现在看看我得到了哪些性能值(同样,我是在一个函数中依次运行所有四个基准):

代码语言:javascript
复制
_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位IMULs慢?!

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2019-03-21 15:01:48

经过大量的尝试和错误,以及互联网上更多的广泛研究,我似乎找到了这种奇怪的性能行为的原因。神奇的词是函数入口点的thunking。但让我从头开始。

我所做的一个观察是,使用哪一个编译器内在来将我的基准测试结果倒过来并不重要。实际上,只要在函数中的任何地方放置一个__nop() (CPU NOP操作码)就可以触发这种效果。即使它被放置在return之前,它也能工作。更多的测试表明,这种效果仅限于包含本征函数的函数。__nop()对代码流不做任何操作,但显然它更改了包含函数的属性。

我发现了一个关于堆栈溢出的问题,它似乎解决了一个类似的问题:如何最好地避免C++/CLI本机类型中的双重阻塞在评论中找到了以下附加信息:

在我们的基础库中,我自己的一个类--使用MFC --被调用了大约一百万次。我们看到了大量零星的性能问题,并且启动了分析器--我可以在这条链的底部看到一声巨响。这种方法调用花费的时间比方法调用的时间要长。

这也正是我所观察到的--函数调用过程中的“某样东西”花费的时间大约是我的代码的四倍。函数块在clrcall修饰符文档和一篇关于双击的文章中作了一定程度的解释。在前一种情况中,有一个提示是使用本质的副作用:

只要__clrcall函数有C++实现,就可以从使用/clr编译的现有C++代码中直接调用该函数。__clrcall函数不能直接从具有内联asm和调用CPU特定内部结构的函数调用,例如,即使这些函数是用/clr编译的。

因此,据我所知,包含本质信息的函数失去了它的__clrcall修饰符,当指定/clr编译器开关时会自动添加该修饰符--如果应该将C++函数编译成本机代码,通常就是这种情况。

我并没有完全了解这个thunking和double thunking的细节,但是很明显,它需要使非托管函数从托管函数中调用。但是,可以通过将其嵌入到#pragma managed(push, off) / #pragma managed(pop)对中来关闭每个函数。不幸的是,此#杂注在命名空间块中不起作用,因此可能需要进行一些编辑,以便将其放置在任何应该发生的位置。

我尝试过这个技巧,将我的所有本机多精度代码都放在这个#杂注中,并获得了以下基准测试结果:

代码语言:javascript
复制
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放置到我的多精度乘法器中,将产生的执行时间是不使用C++函数的版本的执行时间的两倍。我的解释是,在我看来,这段代码调用其他类中的非托管函数,这些类在#杂注之外,因此有一个__clrcall修饰符。这似乎又造成了很大的开销。

坦率地说,我厌倦了进一步调查这个问题。尽管带有单一MUL指令的ASM程序似乎比所有其他尝试都要好,但收益并不像预期的那么大,而且,如果不使用这种方法,我的代码就会发生如此多的变化,我认为这不值得麻烦。因此,我将继续我在一开始写的C++函数,它最初注定只是一个占位符,用于更好的.

在我看来,C++/CLI中的ASM接口不受很好的支持,或者我仍然缺少一些基本的东西。也许有一种方法可以让这个函数摆脱ASM函数的阻碍,但到目前为止,我还没有找到解决方案。甚至远距离都没有。

在这里自由地添加你自己的想法和观察--即使它们只是推测性的。我认为这仍然是一个非常有趣的话题,需要更多的调查。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/55266411

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档