【发布时间】:2012-09-17 03:56:31
【问题描述】:
为什么idiv x86 汇编指令将EDX:EAX(64 位)除以给定的寄存器,而其他数学运算(包括乘法)只对单个输入和输出寄存器进行运算?
乘法:
mov eax, 3
imul eax, 5
部门:
mov edx, 0
mov eax, 15
mov ebx, 5
idiv ebx
我知道EDX 用于存储余数,但为什么没有针对这种行为的单独指令?对我来说似乎不一致。
【问题讨论】:
为什么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 的幂的乘法和除法比其他指令要少得多,所以英特尔可能没有想要用完可用于将更频繁使用的指令的操作码。通用程序中的大多数乘法和除法都是 2 的幂;对于这些,您可以使用位移或lea 指令。
【讨论】:
0x2710 (10000) / 0x02 = 0x1388 (5000),如果结果存储为像除数一样的 8 位数字,则会溢出。英特尔选择 2n 位除以 n 位的结果为 n 位的原因可能只是更简单
IMUL 指令的那个版本被添加到(我认为)80186 以协助地址计算。我不知道为什么它被称为IMUL 而不是MUL,因为操作数和结果大小是相同的,这使得它既不带符号也不带符号。
imul r16, r16, sign-extended-imm8。符号扩展的立即数可能比零扩展更有用,并且使用 IMUL 比使用 MUL 更有意义。此外,IMUL 以对有符号乘法有意义的方式设置标志。请参阅this answer,在那里我更详细地了解了它们在检测溢出方面的不同之处。
还有一个“双倍宽度”乘法(单操作数 mul 或 imul)。
如果您问“为什么没有只给出商的二操作数 idiv”,那么我真的不知道(我有一个理论,但我不为英特尔工作)而且我也希望它存在..
当你想用一个不是 2 的幂的模进行模乘时,效果很好,你可以做一个 mul 并直接用 div 跟进它,一切都已经正确了地方。这是一个结果,而不是一个原因,我们不得不询问英特尔的原因......但这是一个理论。回到 8086 时代,只有双倍宽度乘法(这是一种缓慢的迭代乘法,与您在软件中所做的一样具有提前退出)。后来添加了一些更灵活的乘法,但除法从未发生过。也许它没有那么紧迫 - 毕竟,除法相对较少,而您经常需要乘以小常数,例如索引结构数组。
【讨论】:
imul的一种形式实际上是186的新形式,另一种形式是386。286没有引入任何新形式。
对于加法和减法,您的溢出是由进位标志处理的单个位。如果要取两个任意的 N 位操作数并将它们相乘,则需要 2*N 位来存储结果,非常简单,自己尝试 0xFF * 0xFF = 0xFE01。如果您只使用 N 位大小的寄存器,则乘法指令将非常有限。除法与乘除 2*N 位相反,您得到 N 位。如果您打扰 N 位 * N 位 = 2*N 位数,那么您还应该实现 2*N 位数 / N 位数 = N 位数。这就是它存在的原因,不幸的是,尽管硬件比语言做得更多,但语言应该知道并做到这一点,如果我将两个字节相乘,如果我的结果变量小于 16 位,编译器应该抱怨精度。同时,任何使用加、减、乘或除操作的程序员也应该注意溢出,并且使用这些语言使用的变量是操作数宽度的两倍,这样它们就不会溢出......
【讨论】:
foo = a * (uint64_t)b,而 a 和 b 只是 32 位类型,并且使用单个32 位全乘法而不是完整的 64 x 64 => 64 位乘法,两个输入的上半部分为零。但是,这很烦人,而且他们有时确实会出错(更常见的是除法,当您可以证明单个 2N / N => N 位除法不会溢出时(这会在许多机器上陷入困境,包括 x86),但是编译器仍然调用完整的除法库函数。例如 a*(__uint128_t)b / c 与 c>=b)。
这里有两个问题。首先,存在双倍宽度输入或输出的问题,您忽略了单操作数 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 的操作码相同。 Here's how to use DIV or AAM to turn a 0-99 integer into two ASCII digits,还指出了 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
有符号除法需要更多指令,有时还需要对不太简单的除数的结果进行一些加/减操作。见some examples on Godbolt。即便如此,这还是比硬件除法指令快,which are very slow, like 22-29 cycles latency for DIV r64 on Haswell, with bad throughput
如果他们打算将操作码(和解码器晶体管/电源)用于更多指令,具有单宽度除数的 IDIV 的双寄存器形式可能对编译器有用。
我不太了解硬件除法器是如何在内部实现的,所以 IDK 如果只做 N / N => N 位除法而不是通常的 2N / N => N 可以节省的话。在编译器中输出,几乎所有的划分都是在 CDQ 或xor edx,edx 之后完成的。除法在许多 x86 微架构上是可变延迟的,所以如果在除数实际上只有 N 位时有任何加速,大概硬件已经在寻找它。但是,Skylake DIV/IDIV r32 are constant 26c latency(但 64 位除数要慢得多,并且延迟仍然非常可变)。
大概DIV r32, r32 指令仍会产生 2 个输出(商和余数),我猜在两个输入寄存器中?所以你经常需要额外的 MOV 指令来保存你的输入。或者可能需要立即选择商或余数进入一个目的地,或者使用两个单独的操作码作为商/余数?
此时,他们可以添加一个有点像MULX 的 VEX 编码版本,具有三个显式操作数。但是,MULX 的预期用例是允许扩展精度乘法与扩展精度加法与进位交错,因此 DIVX r64(quotient), r64(remainder), r/m64(divisor)(在 RDX 中带有隐式除数?)将有很大不同(对于扩展精度不太有用)。他们可能仍会将隐性红利设为 RDX:RAX。否则他们甚至不会称它为 DIVX,因为这已经是 a video codec / company 的商标
【讨论】: