我怀疑这是由于结果大于 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++ 代码互操作时,您需要注意调用约定,其中 EAX、ECX 和 EDX 是您的过程的寄存器可以破坏,而您应该保存和恢复其他寄存器的原始值。这意味着避免使用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 always—multiplications are faster than you might think),但您可以在循环中使用它的唯一方法是,如果您首先检查您应该乘以 2,并且进行该检查的成本(更具体地说,作为该检查的结果做出决定的成本)比简单的检查成本远整数乘法。为什么?因为决策需要一个分支,这就引入了mispredictions的可能性。即使您仅在乘数 == 2 的情况下发生错误预测,这也将比 IMUL 和 SHL/ADD 之间的差异更昂贵。
不过,事实上,我们可以为每次乘以 2 的幂做 shl reg, x——这样会更快吗?不,出于同样的原因。实际上,更糟糕的是,因为它会增加错误预测的机会。该条件会按照分支预测算法不太可能理解的模式交替变化,从而导致错误预测的情况多半。