【问题标题】:Strange issue with my factorial program我的阶乘程序的奇怪问题
【发布时间】:2017-12-21 08:37:47
【问题描述】:

我决定学习 x86 汇编作为我的第一门严肃的编程语言。

我决定编写一个程序来计算给定数字的阶乘。

代码可以正常工作,直到大于 12!,之后我得到不正确的结果。

我怀疑这是由于结果大于 32 位。对吗?

为了纠正这个问题,我尝试rcl edx 寄存器。

15!应该是 1307674368000 但它返回 27701857280。

.386
.model flat, stdcall
option casemap :none  

includelib \masm32\lib\msvcrt.lib
sprintf proto C :vararg
includelib \masm32\lib\user32.lib 
MessageBoxA proto :ptr,:ptr,:ptr,:DWORD
includelib \masm32\lib\kernel32.lib
ExitProcess proto :dword 

.data
   format db "%lld", 13, 10, 0
   _title db "Result",13,10,0

.code

main PROC
    LOCAL szBuf[9]:byte
xor edx,edx
rcl edx ,1
xor ebx,ebx



mov eax, 15 ; start of 15!
mov ebx,eax ; Prepares # of loop counter cycle


factoral:

dec ebx     ;loop counter
jz ready    ;when ebx = 0 jump to ready step
imul eax, ebx ; Multiply for intermeddiate result.
rcl edx, 1 ; Shift carry flag to edx to handle > 32 bit results.
jnz factoral ; Continue loop counter when ebx > 0




ready:  
    invoke sprintf, addr szBuf, offset format, eax, edx
    invoke MessageBoxA, 0, addr szBuf, offset _title, 0
    invoke ExitProcess, 0
main ENDP
END main

额外:使用shl eax, 1 来计算中间的第二度部分 (n*2) 比使用 imul 来计算每个度更好。

例如:5!

1) (5*4 =20)

2) (20*3 = 60)

3) (60 左位移 1 次 = 120)

4) (120 * 1 = 120)

【问题讨论】:

  • 是的,问题是由于13!更高的阶乘大于 32 位。但是仅仅包括一个rcl 并不足以让它在 64 位上工作。用 32 位寄存器做 64 位算术比这更复杂。您需要以 64 位值跟踪正在运行的产品。专注于编写一个将 32 位值乘以 64 位值的子程序并使用它。

标签: assembly x86 masm masm32


【解决方案1】:

我怀疑这是由于结果大于 32 位。对吗?

没错。 12! == 479,001,600,可以用 32 位表示(作为无符号数量,但这都是解释,而不是表示形式)。然而,13! == 6,227,020,800,溢出 32 位。如果您使用可以以二进制形式显示数字表示的计算器(Windows、macOS 和大多数 Linux 桌面都内置了这样的程序员计算器),您会看到 64 位表示设置了第 32 位。如果总共只有 32 位,显然它会溢出!

关于您的代码,我不清楚您希望RCL 在这里做什么有用。该指令基本上是通过进位标志(CF)的轮换。它将 CF 移入最低有效位 (LSB),同时将最高有效位 (MSB) 移入 CF。英特尔架构手册对此进行了很好的描述,可能更清楚:

我看不出有任何方法可以帮助您处理大于 32 位的值。我的意思是,IMUL 在乘法导致结果的上半部分携带一个位时,确实 设置了 CF,但旋转不会神奇地让您表示32 位寄存器中的 64 位数量。 (如果这种轮换能让你得到正确的结果,那么英特尔可能只是将它作为乘法的一部分?)

一条指令可用于获得 32 位乘法的 64 位乘积。它也有IMUL 助记符,但它是只接受一个操作数的形式:

IMUL r/m32

这将EAX(硬编码)乘以指定的操作数(r/m32,表示从内存位置读取的 32 位寄存器或 32 位值),将 64-位 导致EDX:EAX(也是硬编码的)。注意EDX:EAX表示法表示高位在EDX中,低位在EAX中。这是在 32 位 x86 架构上表示 64 位值的标准约定。

因此,对代码的简单修复是:

    mov  eax, 13      ; initial value
    mov  ecx, eax     ; loop counter

Factorial:
    dec  ecx          ; decrement counter
    jz   Finished     ; when counter == 0, we're done
    imul ecx          ; multiply by counter (EDX:EAX = EAX * ECX)
    jmp  Factorial    ; go back to top of loop

Finished:
    ...

请注意,我使用ECX 作为计数器,而不是EBX,因为这样更习惯用法。 真的你使用哪个寄存器并不重要,除非指令使用像IMUL这样的硬编码寄存器,但是当它可用时,通常使用ECX作为计数器。 (这是它最初的目的。)另外,当您开始与 C/C++ 代码互操作时,您需要注意调用约定,其中 EAXECXEDX 是您的过程的寄存器可以破坏,而您应该保存和恢复其他寄存器的原始值。这意味着避免使用EBX,除非您绝对需要它可以节省一些代码。

此外,您不需要在初始化之前清除寄存器。因此,代码如下:

xor ebx,ebx
...
mov ebx,eax ; Prepares # of loop counter cycle

是愚蠢的/不必要的。只需发送MOVe。

哦,还有这段代码:

jnz factoral ; Continue loop counter when ebx > 0

从来没有工作过。您试图使用由初始 dec ebx 设置的零标志 (ZF),但其他干预指令破坏了这些标志,因此您没有读取正确的标志值。您需要在之前立即对EBX 进行比较,才能设置标志。

无论如何,在这段代码的末尾,你会在Finished,而阶乘将在EDX:EAX

但是,这仅适用于 13!。之后,它将失败。为什么?因为IMUL 只使用EAX 作为它的被乘数,而不是EDX:EAX。 13×12×11×10×9×8×7×6×5×4×3的乘积正好适合EAX,然后乘以2,乘积正好适合EDX:EAX。但是如果你尝试做 15!,你会在前面溢出到 EDX:EAX,但是 EDX 会被后续的乘法忽略。


因此,您需要变得更聪明,编写实际执行完整 64 位乘法的代码,即将 64 位被乘数乘以 32 位乘法器得到 64 位乘积。

幸运的是,这并不难,尤其是,因为根据定义,阶乘仅适用于非负值,因此我们无需担心负数。换句话说,我们只需要做一个 unsigned 乘法。

顺便说一句,您的printf 格式字符串应该是"%llu",因为结果应该被解释为一个无符号数量。

代码如下:

; EAX = divisor
; ECX = high bits of dividend
; EDX = low bits of dividend
imul  ecx, eax      ; multiply high bits of multiplicand by multiplier, quotient in ECX
mul   edx           ; multiply low bits of multiplicand by multiplier, quotient in EDX:EAX
add   edx, ecx      ; add high-order product to high bits of low-order product
; EDX:EAX = product

最后一条评论的措辞有点毛骨悚然……希望代码具有直观意义。我们所做的只是将乘法分解为两部分,分别对 64 位值的 32 位一半进行运算,然后将结果相加。

将此乘法代码集成到您的原始代码中,我们得到如下结果:

    ;push ebx         ; save EBX (only needed if complying with C calling convention)
    mov  eax, 15      ; initial value (low-order bits)
    xor  edx, edx     ; initial value's high-order bits are 0
    mov  ecx, eax     ; loop counter

Factorial:
    dec  ecx          ; decrement counter
    jz   Finished     ; when counter == 0, we're done
    mov  ebx, ecx     ; make copy of counter
    imul ebx, edx     ; high-order bits * multiplier
    mul  ecx          ; low-order bits * multiplier
    add  edx, ebx     ; add high-order product to high-order bits of low-order product
    jmp  Factorial    ; go back to top of loop

Finished:
    ;pop  ebx         ; restore EBX (only needed if complying with C calling convention)
    ...

这行得通!至少,它可以一直工作到 20 !在 21!,由于我们的老朋友溢出,你得到了错误的结果。 21!不适合 64 位值。

它也不适用于 0!——而不是 1 的数学定义结果,你得到 0。你应该能够插入必要的比较和分支来自己解决这个问题。


有一些方法可以进一步优化此代码,但代价是引入了额外的复杂性,因此请确保您首先了解这一点

我已经提到的一个优化是确保您不会进行最终乘以 1。这只需要在循环体的末尾插入一个额外的比较:

    ;push ebx         ; save EBX (only needed if complying with C calling convention)
    mov  eax, 15      ; initial value (low-order bits)
    xor  edx, edx     ; initial value's high-order bits are 0
    mov  ecx, eax     ; loop counter

Factorial:
    dec  ecx          ; decrement counter
    jz   Finished     ; when counter == 0, we're done
    mov  ebx, ecx     ; make copy of counter
    imul ebx, edx     ; high-order bits * multiplier
    mul  ecx          ; low-order bits * multiplier
    add  edx, ebx     ; add high-order product to high-order bits of low-order product
    cmp  ecx, 1
    jg   Factorial    ; keep looping as long as counter > 1

Finished:
    ;pop  ebx         ; restore EBX (only needed if complying with C calling convention)
    ...

您可以通过将初始比较提升到循环之外来稍微改进这一点:

    ;push ebx         ; save EBX (only needed if complying with C calling convention)
    mov  eax, 15      ; initial value (low-order bits)
    xor  edx, edx     ; initial value's high-order bits are 0
    mov  ecx, eax     ; loop counter
    dec  ecx          ; decrement counter
    jz   Finished     ; when counter == 0, we're done, so skip the loop

Factorial:
    mov  ebx, ecx     ; make copy of counter
    imul ebx, edx     ; high-order bits * multiplier
    mul  ecx          ; low-order bits * multiplier
    add  edx, ebx     ; add high-order product to high-order bits of low-order product
    dec  ecx          ; decrement counter
    jg   Factorial    ; keep looping as long as counter > 1

Finished:
    ;pop  ebx         ; restore EBX (only needed if complying with C calling convention)
    ...

通过简单的优化就可以做到这一点。对于其他想法,您可以explore what C compilers emit for similar code,但请注意,此代码的大部分内容都非常重要。 (GCC 6.3 的输出看起来很像我的代码,但 GCC 7.1 展开循环以获得更快的速度,但导致代码更加混乱和难以阅读/理解。)除此之外,还要注意 C 编译器不会必须有完美的优化器!通常情况下,专业的汇编程序员可以编写比编译器生成的更优化的代码(尽管他们不能这么快!)。


额外:使用 shl eax, 1 来计算中间的第二度部分 (n*2) 会比对每个度都使用 imul 更好。

没有。

首先,你真的不想写shl reg, 1,除非你真的需要设置进位标志。左移 1 相当于乘以 2,相当于将值与自身相加。所以,add reg, reg 更简单、更好、更快。

但是,在这种情况下,即使这样也不会更好。虽然简单的移位或加法确实比乘法更快(但not alwaysmultiplications are faster than you might think),但您可以在循环中使用它的唯一方法是,如果您首先检查您应该乘以 2,并且进行该检查的成本(更具体地说,作为该检查的结果做出决定的成本)比简单的检查成本整数乘法。为什么?因为决策需要一个分支,这就引入了mispredictions的可能性。即使您在乘数 == 2 的情况下发生错误预测,这也将比 IMULSHL/ADD 之间的差异更昂贵。

不过,事实上,我们可以为每次乘以 2 的幂做 shl reg, x——这样会更快吗?不,出于同样的原因。实际上,更糟糕的是,因为它会增加错误预测的机会。该条件会按照分支预测算法不太可能理解的模式交替变化,从而导致错误预测的情况多半。

【讨论】:

  • 由于阶乘也会很快耗尽 64 位(我认为是 21 位),查找表可能值得考虑。在缓存未命中的情况下,它可能很容易比乘法版本慢,但如果代码会经常使用阶乘函数以将表保留在缓存中......
  • 是的,20!尽可能高。在 21! 处,您会溢出 64 位。我在回答中提到了这一点。就查找表而言,这在某人汇编语言时不是很有用。 :-) 但是,如果您发现自己需要编写计算阶乘的真实代码,那将是一个可行的解决方案。 (不过,我的想象力使我无法想象,这个例子可能对范围有限的整数有用。)LUT 可能比此处所做的循环中的乘法更快。如果您展开它或使用 SIMD 指令,那可能更像是一场比赛。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-22
  • 2011-03-10
  • 1970-01-01
  • 2016-01-04
相关资源
最近更新 更多