如果您的模数/除数是已知常数,并且您关心性能,请参阅this and this。对于直到运行时才知道的循环不变值,乘法逆甚至是可能的,例如看https://libdivide.com/(但是如果没有 JIT 代码生成,这比仅对一个常量所需的步骤进行硬编码的效率要低。)
切勿使用div
对于已知的 2 次方:much慢于and
对于余数,或者右移对于除法。查看 C 编译器输出,了解无符号或有符号除以 2 的幂的示例,例如在 Godbolt 编译器资源管理器上。如果您知道运行时输入是 2 的幂,请使用lea eax, [esi-1]
; and eax, edi
或者类似的事情要做x & (y-1)
。 Modulo 256 甚至更高效:movzx eax, cl
在最新的 Intel CPU 上具有零延迟(移动消除),只要两个寄存器是分开的。
在简单/一般情况下:运行时的未知值
The DIV操作说明(及其对应的IDIV对于有符号数)给出商和余数。对于无符号,余数和模是同一件事。供签署idiv
,它给你余数(不是模数)可以为负数:
e.g. -5 / 2 = -2 rem -1
。 x86 除法语义与 C99 完全匹配%
操作员。
DIV r32
将 64 位数字除以EDX:EAX
通过 32 位操作数(在任何寄存器或存储器中)并将商存储在EAX
和剩余的EDX
。它因商溢出而出错。
无符号 32 位示例(在任何模式下都有效)
mov eax, 1234 ; dividend low half
mov edx, 0 ; dividend high half = 0. prefer xor edx,edx
mov ebx, 10 ; divisor can be any register or memory
div ebx ; Divides 1234 by 10.
; EDX = 4 = 1234 % 10 remainder
; EAX = 123 = 1234 / 10 quotient
在 16 位汇编中你可以这样做div bx
将 32 位操作数除以DX:AX
by BX
。参见英特尔的架构软件开发人员手册了解更多信息。
通常总是使用xor edx,edx
未签名之前div
将 EAX 零扩展为 EDX:EAX。这就是“正常”32 位/32 位 => 32 位除法的方法。
对于签名分割,use cdq
before idiv
to sign- 将 EAX 扩展为 EDX:EAX。也可以看看为什么在使用 DIV 指令之前 EDX 应该为 0?。对于其他操作数大小,请使用cbw
(AL->AX),cwd
(AX->DX:AX),cdq
(EAX->EDX:EAX),或cqo
(RAX->RDX:RAX) 将上半部分设置为0
or -1
根据低半部分的符号位。
div
/ idiv
操作数大小为 8、16、32 和(在 64 位模式下)64 位。在当前的 Intel CPU 上,64 位操作数大小比 32 位或更小的速度慢得多,但 AMD CPU 只关心数字的实际大小,而不考虑操作数大小。
请注意,8 位操作数大小很特殊:隐式输入/输出位于 AH:AL(又名 AX)中,而不是 DL:AL 中。看DOSBox 上的 8086 程序集: idiv 指令有错误?举个例子。
有符号 64 位除法示例(需要 64 位模式)
mov rax, 0x8000000000000000 ; INT64_MIN = -9223372036854775808
mov ecx, 10 ; implicit zero-extension is fine for positive numbers
cqo ; sign-extend into RDX, in this case = -1 = 0xFF...FF
idiv rcx
; quotient = RAX = -922337203685477580 = 0xf333333333333334
; remainder = RDX = -8 = 0xfffffffffffffff8
限制/常见错误
div dword 10
不可编码到机器代码中(因此您的汇编器将报告有关无效操作数的错误)。
不同于与mul
/imul
(通常应该使用更快的 2 操作数imul r32, r/m32
或 3 个操作数imul r32, r/m32, imm8/32
相反,不要浪费时间编写高半结果),没有更新的操作码用于除以立即数,或 32 位/32 位 => 32 位除法或没有高半被除数输入的余数。
除法是如此缓慢并且(希望如此)罕见,以至于他们没有费心添加一种方法来让您避免 EAX 和 EDX,或者直接使用立即数。
如果商不适合一个寄存器,div 和 idiv 将出错(AL / AX / EAX / RAX,与股息宽度相同)。这包括除以零,但也适用于非零 EDX 和较小的除数。这就是为什么 C 编译器只是进行零扩展或符号扩展,而不是将 32 位值拆分为 DX:AX。
还有为什么INT_MIN / -1
是 C 未定义行为:它会溢出 2 的补码系统(如 x86)上的有符号商。看为什么整数除以 -1(负一)会得到 FPE?有关 x86 与 ARM 的示例。 x86idiv
在这种情况下确实有错误。
x86 例外是#DE
- 除法例外。在 Unix/Linux 系统上,内核向导致 #DE 异常的进程传递 SIGFPE 算术异常信号。 (在哪些平台上整数除以零会触发浮点异常?)
For div
,使用股息high_half < divisor
是安全的。例如0x11:23 / 0x12
小于0xff
所以它适合 8 位商。
可以通过使用一个块的余数作为下一个块的上半除数 (EDX) 来实现大数除以小数的扩展精度。这可能就是为什么他们选择余数 = EDX 商 = EAX,而不是相反。