为什么GCC在实现整数除法时使用奇数乘法?

为什么GCC在实现整数除法时使用奇数乘法?,c,gcc,assembly,x86-64,integer-division,C,Gcc,Assembly,X86 64,Integer Division,我一直在读关于div和mul汇编操作的书,我决定用C编写一个简单的程序,看看它们的实际操作: 档案部.c 但是查看生成的division.s文件,它不包含任何div操作!取而代之的是,它通过位移动和幻数来实现某种黑魔法。下面是一个计算i/5的代码片段: mov rax, QWORD PTR [rbp-16] ; Move i (=9) to RAX movabs rdx, -3689348814741910323 ; Move some magic number to RDX (?

我一直在读关于
div
mul
汇编操作的书,我决定用C编写一个简单的程序,看看它们的实际操作:

档案部.c 但是查看生成的
division.s
文件,它不包含任何div操作!取而代之的是,它通过位移动和幻数来实现某种黑魔法。下面是一个计算
i/5
的代码片段:

mov     rax, QWORD PTR [rbp-16]   ; Move i (=9) to RAX
movabs  rdx, -3689348814741910323 ; Move some magic number to RDX (?)
mul     rdx                       ; Multiply 9 by magic number
mov     rax, rdx                  ; Take only the upper 64 bits of the result
shr     rax, 2                    ; Shift these bits 2 places to the right (?)
mov     QWORD PTR [rbp-8], rax    ; Magically, RAX contains 9/5=1 now, 
                                  ; so we can assign it to j

这是怎么回事?为什么GCC根本不使用div?它是如何产生这个神奇的数字的?为什么一切都能工作?

整数除法是现代处理器上可以执行的最慢的算术运算之一,延迟高达几十个周期,吞吐量也很差。(对于x86,请参阅)

如果您提前知道除数,可以通过使用一组具有同等效果的其他操作(乘法、加法和移位)替换它来避免除数。即使需要几次运算,它通常也比整数除法本身快得多

以这种方式实现C
/
运算符,而不是使用包含
div
的多指令序列,这只是GCC按常量进行除法的默认方式。它不需要跨操作进行优化,甚至在调试时也不会更改任何内容。(对于较小的代码大小使用
-Os
确实会让GCC使用
div
。)使用乘法逆代替除法就像使用
lea
代替
mul
add

因此,如果除数在编译时是未知的,您只会在输出中看到
div
idiv


有关编译器如何生成这些序列的信息,以及让您自己生成序列的代码(除非您使用的是死气沉沉的编译器,否则几乎不需要),请参阅。

整数除法是现代处理器上可以执行的最慢的算术运算之一,延迟高达几十个周期,吞吐量差。(对于x86,请参阅)

如果您提前知道除数,可以通过使用一组具有同等效果的其他操作(乘法、加法和移位)替换它来避免除数。即使需要几次运算,它通常也比整数除法本身快得多

以这种方式实现C
/
运算符,而不是使用包含
div
的多指令序列,这只是GCC按常量进行除法的默认方式。它不需要跨操作进行优化,甚至在调试时也不会更改任何内容。(对于较小的代码大小使用
-Os
确实会让GCC使用
div
。)使用乘法逆代替除法就像使用
lea
代替
mul
add

因此,如果除数在编译时是未知的,您只会在输出中看到
div
idiv


有关编译器如何生成这些序列的信息,以及让您自己生成这些序列的代码(除非您使用的是死气沉沉的编译器,否则几乎没有必要),请参阅。

除以5等于乘以1/5,这同样等于乘以4/5并右移2位。相关值为十六进制的
ccccccccc d
,如果放在十六进制点后,则为4/5的二进制表示(即五分之四的二进制为
0.11001100
循环-原因见下文)。我想你可以从这里拿走它!您可能想要签出(尽管注意,它在末尾被四舍五入为整数)

至于为什么乘法比除法快,当除数固定时,这是一种更快的方法

有关其工作原理的详细说明,请参阅,并从定点的角度进行解释。它显示了求倒数的算法是如何工作的,以及如何处理有符号除法和模


让我们考虑一下为什么<代码> 0。ccccccCC…<代码>(HEX)或<代码> 0.110011001100…<代码>二进制是4/5。将二进制表示除以4(右移2位)。,我们将得到

0.001100110011…
,通过简单的检查,可以将其添加到原始的
0.111111111…
,它显然等于1,同样的方法
0.9999999…
在十进制中等于1。因此,我们知道
x+x/4=1
,因此
5x/4=1
x=4/5
。然后重新计算以十六进制表示为
cccc d
,用于四舍五入(因为最后一个出现的二进制数字之后将是
1
).

除以5等于乘以1/5,这同样等于乘以4/5并右移2位。所涉及的值是十六进制的
ccccccd
,如果放在十六进制点之后,则是4/5的二进制表示(也就是说,五分之四的二进制是
0.11001100
recurtive-原因见下文)。我想你可以从这里开始使用它!你可能想签出(不过请注意,它在末尾被四舍五入为整数)

至于为什么乘法比除法快,当除数固定时,这是一种更快的方法

有关其工作原理的详细说明,请参阅,并从定点的角度进行解释。它显示了求倒数的算法是如何工作的,以及如何处理有符号除法和模

让我们考虑一下为什么<代码> 0。ccccccCC…<代码>(HEX)或<代码> 0.110011001100…<代码>二进制是4/5。将二进制表示除以4(右移2位)。,我们将得到

0.001100110011…
,通过简单的检查,可以将原始数据添加到
0.111111111…
,这显然等于1,同样的方法
0.999999…
在小数点处等于1。因此,我们知道
gcc -S division.c -O0 -masm=intel
mov     rax, QWORD PTR [rbp-16]   ; Move i (=9) to RAX
movabs  rdx, -3689348814741910323 ; Move some magic number to RDX (?)
mul     rdx                       ; Multiply 9 by magic number
mov     rax, rdx                  ; Take only the upper 64 bits of the result
shr     rax, 2                    ; Shift these bits 2 places to the right (?)
mov     QWORD PTR [rbp-8], rax    ; Magically, RAX contains 9/5=1 now, 
                                  ; so we can assign it to j
; upper 8 bytes of dividend = 2^(ℓ) = (upper part of 2^(N+ℓ))
; lower 8 bytes of dividend for mlow  = 0
; lower 8 bytes of dividend for mhigh = 2^(N+ℓ-prec) = 2^(ℓ+shpre) = 2^(ℓ+e)
dividend  dq    2 dup(?)        ;16 byte dividend
divisor   dq    1 dup(?)        ; 8 byte divisor

; ...
        mov     rcx,divisor
        mov     rdx,0
        mov     rax,dividend+8     ;upper 8 bytes of dividend
        div     rcx                ;after div, rax == 1
        mov     rax,dividend       ;lower 8 bytes of dividend
        div     rcx
        mov     rdx,1              ;rdx:rax = N+1 bit value = 65 bit value
;       rax = dividend, rbx = 64 bit (or less) multiplier, rcx = post shift count
;       two instruction sequence for most divisors:

        mul     rbx                     ;rdx = upper 64 bits of product
        shr     rdx,cl                  ;rdx = quotient
;
;       five instruction sequence for divisors like 7
;       to emulate 65 bit multiplier (rbx = lower 64 bits of multiplier)

        mul     rbx                     ;rdx = upper 64 bits of product
        sub     rbx,rdx                 ;rbx -= rdx
        shr     rbx,1                   ;rbx >>= 1
        add     rdx,rbx                 ;rdx = upper 64 bits of corrected product
        shr     rdx,cl                  ;rdx = quotient
;       ...