# 24章 使用x64下的SIMD來處理浮點數
當然,在增加了x64擴展這個特性之后,FPU在x86兼容處理器中還是存在的。但是同事,SIMD擴展(SSE, SSE2等)已經有了,他們也可以處理浮點數。數字格式依然相同(使用IEEE754標準)。
所以,x86-64編譯器通常都使用SIMD指令。可以說這是一個好消息,因為這讓我們可以更容易的使用他們。 24.1 簡單的例子
```
#!cpp
double f (double a, double b)
{
return a/3.14 + b*4.1;
};
```
清單24.1: MSFC 2012 x64 /Ox
```
#!bash
__real@4010666666666666 DQ 04010666666666666r ; 4.1
__real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
a$ = 8
b$ = 16
f PROC
divsd xmm0, QWORD PTR __real@40091eb851eb851f
mulsd xmm1, QWORD PTR __real@4010666666666666
addsd xmm0, xmm1
ret 0
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
```
#!bash
__real@4010666666666666 DQ 04010666666666666r ; 4.1
__real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
a$ = 8
b$ = 16
f PROC
movsdx QWORD PTR [rsp+16], xmm1
movsdx QWORD PTR [rsp+8], xmm0
movsdx xmm0, QWORD PTR a$[rsp]
divsd xmm0, QWORD PTR __real@40091eb851eb851f
movsdx xmm1, QWORD PTR b$[rsp]
mulsd xmm1, QWORD PTR __real@4010666666666666
addsd xmm0, xmm1
ret 0
f ENDP
```
有一些繁雜,輸入參數保存在“shadow space”(影子空間,7.2.1節),但是只有低一半的寄存器,也即只有64位存了這個double的值。
GCC編譯器生成了幾乎一樣的代碼。
## 24.2 通過參數傳遞浮點型變量
```
#!cpp
#include <math.h>
#include <stdio.h>
int main ()
{
printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54));
return 0;
}
```
他們通過XMM0-XMM3的低一半寄存器傳遞。
清單24.3: MSVC 2012 x64 /Ox
```
#!bash
$SG1354 DB ’32.01 ^ 1.54 = %lf’, 0aH, 00H
__real@40400147ae147ae1 DQ 040400147ae147ae1r ; 32.01
__real@3ff8a3d70a3d70a4 DQ 03ff8a3d70a3d70a4r ; 1.54
main PROC
sub rsp, 40 ; 00000028H
movsdx xmm1, QWORD PTR __real@3ff8a3d70a3d70a4
movsdx xmm0, QWORD PTR __real@40400147ae147ae1
call pow
lea rcx, OFFSET FLAT:$SG1354
movaps xmm1, xmm0
movd rdx, xmm1
call printf
xor eax, eax
add rsp, 40 ; 00000028H
ret 0
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
```
#!bash
.LC2:
.string "32.01 ^ 1.54 = %lf\n"
main:
sub rsp, 8
movsd xmm1, QWORD PTR .LC0[rip]
movsd xmm0, QWORD PTR .LC1[rip]
call pow
; result is now in XMM0
mov edi, OFFSET FLAT:.LC2
mov eax, 1 ; number of vector registers passed
call printf
xor eax, eax
add rsp, 8
ret
.LC0:
.long 171798692
.long 1073259479
.LC1:
.long 2920577761
.long 1077936455
```
GCC讓結果更清晰,printf()的值傳入到了XMM0中。順帶一提,這是一個因為printf()才把1寫入EAX中的例子。這意味著參數會被傳遞到向量寄存器中,就像標準需求一樣(見21章)。
## 24.3 比較式的例子
```
#!cpp
double d_max (double a, double b)
{
if (a>b)
return a;
return b;
};
```
清單 24.5: MSVC 2012 x64 /Ox
```
#!bash
a$ = 8
b$ = 16
d_max PROC
comisd xmm0, xmm1
ja SHORT $LN2@d_max
movaps xmm0, xmm1
$LN2@d_max:
fatret 0
d_max ENDP
```
優化過的MSVC產生了很容易理解的代碼。 COMISD是“Compare Scalar Ordered Double-Precision Floating-Point Values and Set EFLAGS”(比較標量雙精度浮點數的值然后設置EFLAG)的縮寫,顯然,看著名字就知道他要干啥了。 非優化的MSVC代碼產生了更加豐富的代碼,但是仍然不難理解:
清單 24.6: MSVC 2012 x64
```
#!bash
a$ = 8
b$ = 16
d_max PROC
comisd xmm0, xmm1
ja SHORT $LN2@d_max
movaps xmm0, xmm1
$LN2@d_max:
fatret 0
d_max ENDP
```
但是,GCC 4.4.6生成了更多的優化代碼,并且使用了MAXSD(“Return Maximum Scalar Double-Precision Floating-Point Value”,返回最大的雙精度浮點數的值)指令,它將選中其中一個最大數。
清單24.7: GCC 4.4.6 x64 -O3
```
#!bash
a$ = 8
b$ = 16
d_max PROC
movsdx QWORD PTR [rsp+16], xmm1
movsdx QWORD PTR [rsp+8], xmm0
movsdx xmm0, QWORD PTR a$[rsp]
comisd xmm0, QWORD PTR b$[rsp]
jbe SHORT $LN1@d_max
movsdx xmm0, QWORD PTR a$[rsp]
jmp SHORT $LN2@d_max
$LN1@d_max:
movsdx xmm0, QWORD PTR b$[rsp]
$LN2@d_max:
fatret 0
d_max ENDP
```
## 24.4 總結
只有低一半的XMM寄存器會被使用,一組IEEE754格式的數字也會被存在這里。 顯然,所有的指令都有SD后綴(標量雙精度數),這些操作數是可以用于IEEE754浮點數的,他們存在XMM寄存器的低64位中。 比FPU更簡單的是,顯然SIMD擴展并不像FPU以前那么混亂,棧寄存器模型也沒使用。 如果你像試著將例子中的double替換成float的話,它們還是會使用同樣的指令,但是后綴是SS(標量單精度數),例如MOVSS,COMISS,ADDSS等等。 標量(Scalar)代表著SIMD寄存器會包含僅僅一個值,而不是所有的。可以在所有類型的值中生效的指令都被“封裝”成同一個名字。
- 第一章 CPU簡介
- 第二章 Hello,world!
- 第三章? 函數開始和結束
- 第四章 棧
- Chapter 5 printf() 與參數處理
- Chapter 6 scanf()
- CHAPER7 訪問傳遞參數
- Chapter 8 一個或者多個字的返回值
- Chapter 9 指針
- Chapter 10 條件跳轉
- 第11章 選擇結構switch()/case/default
- 第12章 循環結構
- 第13章 strlen()
- Chapter 14 Division by 9
- chapter 15 用FPU工作
- Chapter 16 數組
- Chapter 17 位域
- 第18章 結構體
- 19章 聯合體
- 第二十章 函數指針
- 第21章 在32位環境中的64位值
- 第二十二章 SIMD
- 23章 64位化
- 24章 使用x64下的SIMD來處理浮點數
- 25章 溫度轉換
- 26章 C99的限制
- 27章 內聯函數
- 第28章 得到不正確反匯編結果
- 第29章 花指令
- 第30章 16位Windows
- 第31章 類
- 三十二 ostream