为什么idiv
x86 汇编指令将EDX:EAX
(64 位)除以给定的寄存器,而其他数学运算(包括乘法)仅对单个输入和输出寄存器进行操作?
乘法:
mov eax, 3
imul eax, 5
分配:
mov edx, 0
mov eax, 15
mov ebx, 5
idiv ebx
我知道它EDX
用于存储剩余部分,但为什么没有针对此行为的单独说明?这对我来说似乎不一致。
指令集提供了有效实现任意宽度整数运算所需的指令。对于加法和减法,除了固定宽度结果之外,您只需要知道操作是否导致进位(用于加法)或借位(用于减法)。这就是为什么有一个进位标志。对于乘法,您需要能够将两个字相乘并得到一个双字结果。这就是为什么imul
在edx:eax
. 对于除法,您需要能够除以一个双角数并得到商和余数。
要了解为什么需要这些特定操作,请参阅 Knuth 的《计算机编程艺术》第 2 卷,其中详细介绍了实现任意宽度算术的算法。
至于为什么x86指令集中没有更多不同形式的乘除指令,不是2的幂的乘除比其他指令少得多,所以英特尔可能不想使用up 可用于将更频繁使用的指令的操作码。通用程序中的大多数乘法和除法都是 2 的幂;对于这些,您可以使用位移位或lea
指令。
还有一个“双宽度”乘法(单操作数mul
或imul
)。
如果您问“为什么没有idiv
仅给出商的二操作数”,那么我真的不知道(我有一个理论,但我不为英特尔工作)并且我希望它也存在..
当你想用不是 2 的幂的模进行模乘时,它的效果很好,你可以做 amul
并直接用 a 跟进它div
,一切都已经在正确的位置。这是一个结果,而不是一个原因,我们不得不问英特尔的原因......但这是一个理论。回到 8086 时代,只有双倍宽度乘法(它是一种缓慢的迭代乘法,具有与您在软件中所做的相同的提前退出)。后来添加了一些更灵活的乘法,但除法从未发生过。也许它没有那么紧迫 - 毕竟,除法相对较少,而您经常需要乘以小常数,例如索引结构数组。
对于加法和减法,您的溢出是由进位标志处理的单个位。如果您要取两个任意 N 位操作数并将它们相乘,则需要 2*N 位来存储结果,非常简单,自己尝试 0xFF * 0xFF = 0xFE01。如果您只使用 N 位大小的寄存器,则乘法指令将非常有限。除法与乘除 2*N 位相反,您得到 N 位。如果您打扰 N 位 * N 位 = 2*N 位数,那么您还应该实现 2*N 位数 / N 位数 = N 位数。这就是它存在的原因,不幸的是,尽管硬件比语言做得更多,但语言应该知道并做到这一点,如果我将两个字节相乘,如果我的结果变量小于 16 位,编译器应该会抱怨精度。
这里有两个问题。首先,存在双宽度输入或输出的问题,您忽略了进行完全扩展乘法的单操作数MUL / IMUL形式,包括结果的高半部分:N * N => 2N 位,执行 EDX:EAX = EAX * src
. 请参阅其他答案,了解为什么这很有用。
BMI2 甚至引入了更灵活的全乘指令MULX,它具有三个显式操作数(两个输出和一个输入)和一个隐式操作数(第二个源 = EDX)。
其次,您举了一个使用imul
立即操作数的示例,这是 DIV/IDIV 不可用的另一件事。
有一条晦涩的指令实际上是一个立即数,执行 8 位 / imm8 => 8 位商/余数,而不是 16 / 8 => 8。它称为AAM,在 64 位模式下不可用。汇编器默认除以 10(对于 BCD 的预期用例),但它与任何 imm8 的操作码相同。 以下是如何使用 DIV 或 AAM 将 0-99 整数转换为两个 ASCII 数字,同时指出 AAM 和DIV r/m8
.
英特尔本可以随时添加 IDIV 的即时版本,但从未这样做过。我的猜测是 DIV / IDIV 足够慢(并且足够罕见)以至于额外的开销mov reg, imm32
可以忽略不计,并且在这样的指令上花费操作码空间(和解码器晶体管)从未被认为值得。
更重要的是,编译时常量的实际硬件划分通常只对代码大小有用,而不是性能。自 90 年代以来,模乘法逆运算一直是众所周知的(编译器编写者)。由于编译器甚至不使用常量除法,英特尔极不可能在这种技术为人所知后设计的 CPU 中为其添加指令。例如 clang 编译unsigned int div10(unsigned int a) { return a/10; }
为
mov ecx, edi # just to zero-extend to 64-bit
mov eax, 3435973837 # a sign-extended imm32 can't represent this constant, I guess. clang uses imul r,r,imm for other cases.
imul rax, rcx # 64-bit multiply instead of 32x32 => 64 in two separate regs
shr rax, 35 # extract part of the high-half result.
ret
有符号除法需要更多指令,有时需要一些加/减操作来处理不太简单的除数的结果。请参阅有关 Godbolt 的一些示例。即便如此,这还是比硬件除法指令快,后者非常慢,例如DIV r64
在 Haswell 上的延迟为 22-29 个周期,吞吐量很差
如果他们打算在更多指令上花费操作码(和解码器晶体管/电源),那么具有单宽度除数的两寄存器形式的 IDIV 可能对编译器有用。
我不太了解硬件除法器是如何在内部实现的,所以 IDK 如果只做 N / N => N 位除法而不是通常的 2N / N => N 可以节省的话。在编译器输出中,几乎所有除法都在 CDQ 或xor edx,edx
. 除法在许多 x86 微架构上是可变延迟的,所以如果在除数实际上只有 N 位时有任何加速,大概硬件已经在寻找它了。但是,Skylake DIV/IDIV r32 是恒定的 26c 延迟(但 64 位除数要慢得多,并且延迟仍然非常可变)。
大概一条DIV r32, r32
指令仍会产生 2 个输出(商和余数),我猜在两个输入寄存器中?所以你经常需要额外的 MOV 指令来保存你的输入。或者可能需要立即选择商或余数进入一个目的地,或者对商/余数使用两个单独的操作码?
此时,他们可以添加一个有点像MULX的 VEX 编码版本,具有三个显式操作数。但是,MULX 的预期用例是允许扩展精度乘法与扩展精度加法与进位交错,因此DIVX r64(quotient), r64(remainder), r/m64(divisor)
(在 RDX 中有隐式除数?)将有很大不同(对扩展精度不太有用)。他们可能仍会将隐性红利设为 RDX:RAX。否则他们甚至不会称它为 DIVX,因为这已经是视频编解码器/公司的商标