为什么x86汇编中的64位被分红?

时间:2012-09-25 15:29:51

标签: assembly x86

为什么idiv x86汇编指令将EDX:EAX(64位)除以给定寄存器,而其他数学运算(包括乘法)只对单个输入和输出寄存器进行操作?

乘:

mov eax, 3
imul eax, 5

司:

mov edx, 0
mov eax, 15
mov ebx, 5
idiv ebx

我知道EDX用于存储余数,但为什么没有针对此行为的单独指令?这对我来说似乎不一致。

4 个答案:

答案 0 :(得分:6)

指令集提供了有效实现任意宽度整数运算所必需的指令。对于加法和减法,除了固定宽度结果之外,您需要知道的是操作是否导致进位(用于加法)或借用(用于减法)。这就是进位标志的原因。对于乘法,您需要能够将两个单词相乘并获得双字结果。这就是imuledx:eax中生成结果的原因。对于除法,您需要能够划分双宽度数并得到商和余数。

要了解您需要这些特定操作的原因,请参阅Knuth的计算机编程艺术,第2卷,其中详细介绍了实现任意宽度算法的算法。

至于为什么x86指令集中没有更多不同形式的乘法和除法指令,乘法和除法不是2的幂,比其他指令要少得多,因此英特尔可能没有想要使用可用于更频繁使用的指令的操作码。通用程序中的大多数乘法和除法都是2的幂;对于这些,您可以使用位移或lea指令。

答案 1 :(得分:4)

还有一个“双倍宽度”乘法(单操作数mulimul)。

如果你问“为什么没有只给出商数的双操作数idiv”,那么我真的不知道(我有一个理论,但我不是为英特尔工作)而且我希望它也存在......

如果你想用一个不是2的幂的模数进行模乘,你可以做mul并直接用div跟踪它并且一切都很好。已经在正确的地方。这是结果,而不是理由,因为我们不得不问英特尔..但这是一个理论。早在8086年代,只有双倍宽度乘法(这是一种缓慢的迭代乘法,早期退出与你在软件中所做的相同)。后来在80286中,他们增加了一些更灵活的乘法,但他们从来没有对分区做同样的事情。也许它并不那么紧迫 - 毕竟,划分是相对罕见的,而你经常需要乘以小常数,例如索引结构数组。

答案 2 :(得分:2)

对于加法和减法,溢出是由进位标志处理的单个位。如果要取两个任意N位操作数并将它们相乘,则需要2 * N位来存储结果,非常简单,自己尝试0xFF * 0xFF = 0xFE01。如果您只使用N位大小的寄存器,则乘法指令将非常有限。除法得到N位,除法乘以2 * N位。如果您打扰N位* N位= 2 * N位,那么您还应该实现2 * N位/ N位= N位。这就是为什么它存在,不幸的是虽然硬件不仅仅是语言,语言本应该知道并完成这个,如果我乘以两个字节,编译器应该抱怨精度,如果我的结果变量小于16位。同时,任何使用加,减,乘或除运算的程序员也应该知道溢出,并使用这些语言使用两倍于操作数宽度的变量,这样它们就不会溢出...

答案 3 :(得分:0)

这里有两个问题。首先,存在双宽输入或输出的问题,并且您忽略了完全加宽乘法的单操作数MUL / IMUL形式,包括高半值结果:N * N => 2N位,执行EDX:EAX = EAX * src。请参阅其他答案,了解这有用的原因。

BMI2甚至引入了更灵活的全乘法指令MULX,它有三个显式操作数(两个输出和一个输入),只有一个隐式操作数(第二个源= EDX)。

其次,您举一个使用立即操作数的示例,该操作数也不适用于DIV / IDIV,并且没有人提到过。

有一个模糊的指令实际上是一个立即div,做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指令来保存输入。或者可能需要立即选择商或余数进入一个目的地,或者使用两个单独的操作码作为商/余数?

此时,他们可以添加一个VEX编码版本,有点像MULX,有三个显式操作数。但是,MULX的预期用例允许扩展精度乘法与扩展精度的随身携带交织,因此DIVX r64(quotient), r64(remainder), r/m64(divisor)(在RDX中具有隐式被除数?)会有很大的不同(对于扩展精度)。他们可能仍然将隐含股息设为RDX:RAX。或者也许他们甚至不会称之为DIVX,因为它已经是a video codec / company的商标