GCC在将整数转换为浮点数时生成的FPU操作

sal*_*eph 5 c assembly gcc x86-64 fpu

我想在C中对FPU执行除法(使用整数值):

float foo;
uint32_t *ptr1, *ptr2;
foo = (float)*(ptr1) / (float)*(ptr2);
Run Code Online (Sandbox Code Playgroud)

NASM中(来自通过GCC编译的对象),它具有以下表示:

    mov     rax, QWORD [ptr1]
    mov     eax, DWORD [rax]
    mov     eax, eax
    test    rax, rax
    js      ?_001
    pxor    xmm0, xmm0
    cvtsi2ss xmm0, rax
    jmp     ?_002

?_001:
    mov     rdx, rax
    shr     rdx, 1
    and     eax, 01H
    or      rdx, rax
    pxor    xmm0, xmm0
    cvtsi2ss xmm0, rdx
    addss   xmm0, xmm0
?_002:
    mov     rax, QWORD [ptr2]

; ... for ptr2 pattern repeats
Run Code Online (Sandbox Code Playgroud)

这个"黑魔法"下的是什么?_001是什么意思?不仅cvtsi2ss足以从整数转换为浮点数吗?

Cod*_*ray 8

您必须查看未经优化的代码.那是浪费时间.当优化器被禁用时,编译器会出于各种原因生成一堆无意义的代码 - 以实现更快的编译速度,更容易在源代码行上设置断点,以便更容易捕获错误等.

当您在针对x86-64的编译器上生成优化代码时,所有这些噪声都消失了,代码变得更加高效,因此,更容易理解/理解.

这是一个执行所需操作的函数.我把它写成一个函数,以便我可以将输入作为不透明参数传递,编译器无法对其进行优化.

float DivideAsFloat(uint32_t *ptr1, uint32_t *ptr2)
{
    return (float)(*ptr1) / (float)(*ptr2);
}
Run Code Online (Sandbox Code Playgroud)

这是GCC的所有版本(返回4.9.0)为此函数生成的目标代码:

DivideAsFloat(unsigned int*, unsigned int*):
    mov        eax, DWORD PTR [rdi]   ; retrieve value of 'ptr1' parameter
    pxor       xmm0, xmm0             ; zero-out xmm0 register
    pxor       xmm1, xmm1             ; zero-out xmm1 register
    cvtsi2ssq  xmm0, rax              ; convert *ptr1 into a floating-point value in XMM0
    mov        eax, DWORD PTR [rsi]   ; retrieve value of 'ptr2' parameter
    cvtsi2ssq  xmm1, rax              ; convert *ptr2 into a floating-point value in XMM1
    divss      xmm0, xmm1             ; divide the two floating-point values
    ret
Run Code Online (Sandbox Code Playgroud)

这几乎就是您期望看到的.这里唯一的"黑魔法"就是PXOR指示.为什么编译器在执行CVTSI2SS指令之前还要先对XMM寄存器进行预置零操作呢?好吧,因为CVTSI2SS只有部分 clobbers它的目的地注册.具体来说,它只破坏较低的位,使高位保持不变.这导致对高位的错误依赖,这导致执行停顿.可以通过将寄存器预置零来打破这种依赖性,从而防止停顿和加速执行的可能性.该PXOR指令是一种快速有效的清除寄存器的方法.(我最近在这里谈到了这个完全相同的现象- 见最后一段.)

事实上,旧版本的GCC(4.9.0之前版本)没有执行此优化,因此生成的代码不包含PXOR指令.它看起来效率更高,但实际上运行速度较慢.

DivideAsFloat(unsigned int*, unsigned int*):
    mov        eax, DWORD PTR [rdi]   ; retrieve value of 'ptr1' parameter
    cvtsi2ssq  xmm0, rax              ; convert *ptr1 into a floating-point value in XMM0
    mov        eax, DWORD PTR [rsi]   ; retrieve value of 'ptr2' parameter
    cvtsi2ssq  xmm1, rax              ; convert *ptr2 into a floating-point value in XMM1
    divss      xmm0, xmm1             ; divide the two floating-point values
    ret
Run Code Online (Sandbox Code Playgroud)

Clang 3.9发出的代码与这些旧版本的GCC相同.它也不知道优化.MSVC确实知道它(自VS 2010以来),ICC的现代版本也是如此(在ICC 16及更高版本上验证;在ICC 13中缺失).

然而,这并不是说Anty的回答(以及Mystical的评论)是完全错误的.CVTSI2SS确实设计用于将有符号整数转换为标量单精度浮点数,而不是像这里一样的无符号整数.什么给出了什么?好吧,64位处理器具有64位宽的寄存器,因此无符号的32位输入值可以存储为带符号的64位中间值,这样CVTSI2SS就可以使用了.

编译器在启用优化时执行此操作,因为它会产生更高效的代码.另一方面,如果您的目标是32位x86并且没有可用的64位寄存器,则编译器必须处理已签名和未签名的问题.以下是GCC 6.3如何处理它:

DivideAsFloat(unsigned int*, unsigned int*):
    sub       esp,  4                 
    pxor      xmm0, xmm0              
    mov       eax,  DWORD PTR [esp+8] 
    pxor      xmm1, xmm1              
    movss     xmm3, 1199570944        
    pxor      xmm2, xmm2              
    mov       eax,  DWORD PTR [eax]   
    movzx     edx,  ax                
    shr       eax,  16                
    cvtsi2ss  xmm0, eax               
    mov       eax,  DWORD PTR [esp+12]
    cvtsi2ss  xmm1, edx               
    mov       eax,  DWORD PTR [eax]   
    movzx     edx,  ax                
    shr       eax,  16                
    cvtsi2ss  xmm2, edx               
    mulss     xmm0, xmm3              
    addss     xmm0, xmm1              
    pxor      xmm1, xmm1              
    cvtsi2ss  xmm1, eax               
    mulss     xmm1, xmm3
    addss     xmm1, xmm2
    divss     xmm0, xmm1
    movss     DWORD PTR [esp], xmm0
    fld       DWORD PTR [esp]
    add       esp,  4
    ret
Run Code Online (Sandbox Code Playgroud)

由于优化器重新排列和交错指令的方式,这有点难以理解.在这里,我"没有优化"它,重新排序指令并将它们分成更多逻辑组,希望能够更容易地遵循执行流程.(我删除的唯一指令是依赖性破坏PXOR- 代码的其余部分是相同的,只是重新排列.)

DivideAsFloat(unsigned int*, unsigned int*):
  ;;; Initialization ;;;
  sub       esp,  4           ; reserve 4 bytes on the stack

  pxor      xmm0, xmm0        ; zero-out XMM0
  pxor      xmm1, xmm1        ; zero-out XMM1
  pxor      xmm2, xmm2        ; zero-out XMM2
  movss     xmm3, 1199570944  ; load a constant into XMM3


  ;;; Deal with the first value ('ptr1') ;;;
  mov       eax,  DWORD PTR [esp+8]  ; get the pointer specified in 'ptr1'
  mov       eax,  DWORD PTR [eax]    ; dereference the pointer specified by 'ptr1'
  movzx     edx,  ax                 ; put the lower 16 bits of *ptr1 in EDX
  shr       eax,  16                 ; move the upper 16 bits of *ptr1 down to the lower 16 bits in EAX
  cvtsi2ss  xmm0, eax                ; convert the upper 16 bits of *ptr1 to a float
  cvtsi2ss  xmm1, edx                ; convert the lower 16 bits of *ptr1 (now in EDX) to a float

  mulss     xmm0, xmm3               ; multiply FP-representation of upper 16 bits of *ptr1 by magic number
  addss     xmm0, xmm1               ; add the result to the FP-representation of *ptr1's lower 16 bits


  ;;; Deal with the second value ('ptr2') ;;;
  mov       eax,  DWORD PTR [esp+12] ; get the pointer specified in 'ptr2'
  mov       eax,  DWORD PTR [eax]    ; dereference the pointer specified by 'ptr2'
  movzx     edx,  ax                 ; put the lower 16 bits of *ptr2 in EDX
  shr       eax,  16                 ; move the upper 16 bits of *ptr2 down to the lower 16 bits in EAX
  cvtsi2ss  xmm2, edx                ; convert the lower 16 bits of *ptr2 (now in EDX) to a float
  cvtsi2ss  xmm1, eax                ; convert the upper 16 bits of *ptr2 to a float

  mulss     xmm1, xmm3               ; multiply FP-representation of upper 16 bits of *ptr2 by magic number
  addss     xmm1, xmm2               ; add the result to the FP-representation of *ptr2's lower 16 bits


  ;;; Do the division, and return the result ;;;
  divss     xmm0, xmm1               ; FINALLY, divide the FP-representation of *ptr1 by *ptr2
  movss     DWORD PTR [esp], xmm0    ; store this result onto the stack, in the memory we reserved
  fld       DWORD PTR [esp]          ; load this result onto the top of the x87 FPU
                                     ;  (the 32-bit calling convention requires floating-point values be returned this way)

  add       esp,  4                  ; clean up the space we allocated on the stack
  ret
Run Code Online (Sandbox Code Playgroud)

请注意,此处的策略是将每个无符号32位整数值分解为两个16位半.上半部分被转换为浮点表示并乘以幻数(以补偿有符号的值).然后,将下半部分转换为浮点表示,并将这两个浮点表示(原始32位值的每个16位一半)相加.对于每个32位输入值,这是两次一次(参见指令的两个"组").然后,最后,分割得到的两个浮点表示,并返回结果.

逻辑类似于未经优化的代码所做的,但是......好吧,更优化.特别是,删除了冗余指令并且对算法进行了推广,因此不需要在签名上进行分支.这会加快速度,因为错误预测的分支很慢.

请注意,Clang使用略有不同的策略,并且能够在此生成比GCC更优化的代码:

DivideAsFloat(unsigned int*, unsigned int*):
   push     eax                       ; reserve 4 bytes on the stack

   mov      eax,  DWORD PTR [esp+12]  ; get the pointer specified in 'ptr2'
   mov      ecx,  DWORD PTR [esp+8]   ; get the pointer specified in 'ptr1'
   movsd    xmm1, QWORD PTR 4841369599423283200 ; load a constant into XMM1

   movd     xmm0, DWORD PTR [ecx]     ; dereference the pointer specified by 'ptr1',
                                      ;  and load the bits directly into XMM0
   movd     xmm2, DWORD PTR [eax]     ; dereference the pointer specified by 'ptr2'
                                      ;  and load the bits directly into XMM2

   orpd     xmm0, xmm1                ; bitwise-OR *ptr1's raw bits with the magic number
   orpd     xmm2, xmm1                ; bitwise-OR *ptr2's raw bits with the magic number

   subsd    xmm0, xmm1                ; subtract the magic number from the result of the OR
   subsd    xmm2, xmm1                ; subtract the magic number from the result of the OR

   cvtsd2ss xmm0, xmm0                ; convert *ptr1 from single-precision to double-precision in place
   xorps    xmm1, xmm1                ; zero register to break dependencies
   cvtsd2ss xmm1, xmm2                ; convert *ptr2 from single-precision to double-precision, putting result in XMM1

   divss    xmm0, xmm1                ; FINALLY, do the division on the single-precision FP values
   movss    DWORD PTR [esp], xmm0     ; store this result onto the stack, in the memory we reserved
   fld       DWORD PTR [esp]          ; load this result onto the top of the x87 FPU
                                      ;  (the 32-bit calling convention requires floating-point values be returned this way)

   pop      eax                       ; clean up the space we allocated on the stack
   ret
Run Code Online (Sandbox Code Playgroud)

它甚至没有使用CVTSI2SS指令!相反,它加载整数位并使用一些魔术比特来操纵它们,因此它可以将其视为双精度浮点值.稍后会有一些比特麻烦,它用于CVTSD2SS将这些双精度浮点值中的每一个转换为单精度浮点值.最后,它划分两个单精度浮点值,并安排返回值.

因此,当针对32位时,编译器必须处理有符号和无符号整数之间的差异,但它们使用不同的策略以不同的方式进行,有些策略可能比其他策略更优化.这就是为什么查看优化代码更具启发性,除了它实际上是在客户机器上执行的事实.


Ant*_*nty 5

一般来说,cvtsi2ss可以做到这一点 - 将标量整数(其他来源命名为双字整数命名为单标量,但我的命名与其他向量标题一致)转换为标量单(浮点数).但它需要有符号整数.

所以这段代码

mov     rdx, rax                                
shr     rdx, 1                                  
and     eax, 01H                                
or      rdx, rax                                
pxor    xmm0, xmm0                              
cvtsi2ss xmm0, rdx                              
addss   xmm0, xmm0  
Run Code Online (Sandbox Code Playgroud)

帮助将unsigned转换为signed(请注意js jump - 如果设置了sign bit,则执行此代码 - 否则跳过它).对于uint32_t,当值大于0x7FFFFFFF时设置符号.

所以"魔术"代码确实:

mov     rdx, rax       ; move value from ptr1 to edx                         
shr     rdx, 1         ; div by 2 - logic shift not arithmetic because ptr1 is unsigned
and     eax, 01H       ; save least significant bit                          
or      rdx, rax       ; move this bit to divided value to someway fix rounding errors                         
pxor    xmm0, xmm0                              
cvtsi2ss xmm0, rdx                              
addss   xmm0, xmm0     ; add to itself = multiply by 2
Run Code Online (Sandbox Code Playgroud)

我不确定你使用什么编译器和什么编译选项 - GCC做的很简单

cvtsi2ssq       xmm0, rbx
cvtsi2ssq       xmm1, rax
divss   xmm0, xmm1
Run Code Online (Sandbox Code Playgroud)

我希望它有所帮助.