23.3 浮点数字

见24章以了解更多的x86-64处理器中是如何处理浮点数的。 # 使用x64下的SIMD来处理浮点数

当然,在增加了x64扩展这个特性之后,FPU在x86兼容处理器中还是存在的。但是同事,SIMD扩展(SSE, SSE2等)已经有了,他们也可以处理浮点数。数字格式依然相同(使用IEEE754标准)。

所以,x86-64编译器通常都使用SIMD指令。可以说这是一个好消息,因为这让我们可以更容易的使用他们。 24.1 简单的例子

  1. double f (double a, double b)
  2. {
  3. return a/3.14 + b*4.1;
  4. };

清单24.1: MSFC 2012 x64 /Ox

  1. __real@4010666666666666 DQ 04010666666666666r ; 4.1
  2. __real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
  3. a$ = 8
  4. b$ = 16
  5. f PROC
  6. divsd xmm0, QWORD PTR __real@40091eb851eb851f
  7. mulsd xmm1, QWORD PTR __real@4010666666666666
  8. addsd xmm0, xmm1
  9. ret 0
  10. f ENDP

输入的浮点数被传入了XMM0-XMM3寄存器,其他的通过栈来传递。 a被传入了XMM0,b则是通过XMM1。 XMM寄存器是128位的(可以参考SIMD22一节),但是我们的类型是double型的,也就意味着只有一半的寄存器会被使用。

DIVSD是一个SSE指令,意思是“Divide Scalar Double-Precision Floating-Point Values”(除以标量双精度浮点数值),它只是把一个double除以另一个double,然后把结果存在操作符的低一半位中。 常量会被编译器以IEEE754格式提前编码。 MULSD和ADDSD也是类似的,只不过一个是乘法,一个是加法。 函数处理double的结果将保存在XMM0寄存器中。

这是无优化的MSVC编译器的结果:

清单24.2: MSVC 2012 x64

  1. __real@4010666666666666 DQ 04010666666666666r ; 4.1
  2. __real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
  3. a$ = 8
  4. b$ = 16
  5. f PROC
  6. movsdx QWORD PTR [rsp+16], xmm1
  7. movsdx QWORD PTR [rsp+8], xmm0
  8. movsdx xmm0, QWORD PTR a$[rsp]
  9. divsd xmm0, QWORD PTR __real@40091eb851eb851f
  10. movsdx xmm1, QWORD PTR b$[rsp]
  11. mulsd xmm1, QWORD PTR __real@4010666666666666
  12. addsd xmm0, xmm1
  13. ret 0
  14. f ENDP

有一些繁杂,输入参数保存在“shadow space”(影子空间,7.2.1节),但是只有低一半的寄存器,也即只有64位存了这个double的值。

GCC编译器生成了几乎一样的代码。

24.2 通过参数传递浮点型变量

  1. #include <math.h>
  2. #include <stdio.h>
  3. int main ()
  4. {
  5. printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54));
  6. return 0;
  7. }

他们通过XMM0-XMM3的低一半寄存器传递。

清单24.3: MSVC 2012 x64 /Ox

  1. $SG1354 DB 32.01 ^ 1.54 = %lf’, 0aH, 00H
  2. __real@40400147ae147ae1 DQ 040400147ae147ae1r ; 32.01
  3. __real@3ff8a3d70a3d70a4 DQ 03ff8a3d70a3d70a4r ; 1.54
  4. main PROC
  5. sub rsp, 40 ; 00000028H
  6. movsdx xmm1, QWORD PTR __real@3ff8a3d70a3d70a4
  7. movsdx xmm0, QWORD PTR __real@40400147ae147ae1
  8. call pow
  9. lea rcx, OFFSET FLAT:$SG1354
  10. movaps xmm1, xmm0
  11. movd rdx, xmm1
  12. call printf
  13. xor eax, eax
  14. add rsp, 40 ; 00000028H
  15. ret 0
  16. main ENDP

在Intel和AMD的手册中(见14章和1章)并没有MOVSDX这个指令,而只有MOVSD一个。所以在x86中有两个指令共享了同一个名字(另一个见B.6.2)。显然,微软的开发者想要避免弄得一团糟,所以他们把它重命名为MOVSDX,它只是会多把一个值载入XMM寄存器的低一半中。 pow()函数从XMM0和XMM1中加载参数,然后返回结果到XMM0中。 然后把值移动到RDX中,因为接下来printf()需要调用这个函数。为什么?老实说我也不知道,也许是因为printf()是一个参数不定的函数?

清单24.4:GCC 4.4.6 x64 -O3

  1. .LC2:
  2. .string "32.01 ^ 1.54 = %lf\n"
  3. main:
  4. sub rsp, 8
  5. movsd xmm1, QWORD PTR .LC0[rip]
  6. movsd xmm0, QWORD PTR .LC1[rip]
  7. call pow
  8. ; result is now in XMM0
  9. mov edi, OFFSET FLAT:.LC2
  10. mov eax, 1 ; number of vector registers passed
  11. call printf
  12. xor eax, eax
  13. add rsp, 8
  14. ret
  15. .LC0:
  16. .long 171798692
  17. .long 1073259479
  18. .LC1:
  19. .long 2920577761
  20. .long 1077936455

GCC让结果更清晰,printf()的值传入到了XMM0中。顺带一提,这是一个因为printf()才把1写入EAX中的例子。这意味着参数会被传递到向量寄存器中,就像标准需求一样(见21章)。