浮點程式碼 (Floating-Point Code)

Overview Table

主題 核心內容 書頁
浮點架構 (FP architecture) 儲存/存取方式、指令集、參數傳遞、暫存器保存慣例四要素;MMX → SSE → AVX 演進 p.329-331
媒體暫存器 16 個 YMM (256-bit),低 128-bit 即 XMM;scalar 模式只用低 32/64 bit p.331
搬移指令 vmovss/vmovsd(記憶體↔XMM)、vmovaps/vmovapd(XMM↔XMM,aligned) p.332
轉換指令 vcvttss2si 系列(FP→int,截斷)、vcvtsi2ss 系列(int→FP,三運算元) p.333-336
程序慣例 前 8 個 FP 參數走 %xmm0%xmm7;回傳值在 %xmm0;XMM 全部 caller-saved p.337-338
算術指令 vaddsdvsubsdvmulsdvdivsdvmaxsdvminsdsqrtsd p.338-340
浮點常數 不能用 immediate,常數存記憶體 .LC 標籤 + %rip 相對定址讀取 p.340
位元運算 vxorps/vandps 等 packed 運算實作歸零、絕對值、取負 p.341-342
比較指令 ucomiss/ucomisd 設定 CF/ZF/PF;NaN → unordered → PF=1,用 jp 偵測 p.342-344
觀察與總結 (3.11.7, 3.12) AVX2 風格類似整數碼;機器碼 vs C;C++/Java 編譯方式對比 p.345-346

3.11 浮點架構總覽 (p.329-330)

處理器的浮點架構 (floating-point architecture) 定義了浮點程式如何映射到機器,包含四個面向:

歷史演進:自 1997 年 Pentium/MMX 起,Intel/AMD 陸續加入媒體指令 (media instructions) 支援圖形與影像處理,核心概念是 SIMD(single instruction, multiple data,唸 "sim-dee")——同一運算平行套用到多筆資料:

世代演進(暫存器名 / 寬度):
MMX ──────────> SSE ────────────────> AVX
"MM" 64-bit     "XMM" 128-bit         "YMM" 256-bit
1997            (SSE2: 2000,          (AVX2: 2013,
                 Pentium 4)            Core i7 Haswell)
每個 YMM 暫存器 256 bit,可容納 8 個 32-bit 值或 4 個 64-bit 值(整數或浮點皆可)——這是 SIMD 平行化的基礎;但 scalar 浮點碼只用最低的 4 或 8 bytes。完整 SIMD 利用見 Web Aside opt:simd 與 05-Program-Optimization/03-Loop-Unrolling-and-Parallelism

媒體暫存器配置 (Figure 3.45, p.331)

255            127              0
+---------------+---------------+
|     %ymm0     |    %xmm0      |  第 1 個 FP 參數 / 回傳值
|     %ymm1     |    %xmm1      |  第 2 個 FP 參數
|      ...      |     ...       |  ...
|     %ymm7     |    %xmm7      |  第 8 個 FP 參數
| %ymm8-%ymm15  | %xmm8-%xmm15  |  Caller saved
+---------------+---------------+
 每個 YMM = 32 bytes;低 16 bytes 可用 XMM 名稱存取
 scalar float 只用低 4 bytes;scalar double 只用低 8 bytes

組合碼以 SSE 的 XMM 名稱 %xmm0%xmm15 引用暫存器;每個 XMM 是對應 YMM 的低 128 bit。


3.11.1 浮點搬移與轉換 (p.332-336)

搬移指令 (Figure 3.46)

指令 Source Destination 說明
vmovss M32 / X X / M32 搬移 single precision
vmovsd M64 / X X / M64 搬移 double precision
vmovaps X X 搬移 aligned packed single
vmovapd X X 搬移 aligned packed double

(X:XMM 暫存器;M32/M64:32/64-bit 記憶體範圍)

指令名的 'a' 代表 "aligned":vmovaps/vmovapd 若用於讀寫記憶體且位址不滿足 16-byte 對齊會觸發 exception;暫存器對暫存器則無對齊問題。

範例 float_mov(p.332-333):

float float_mov(float v1, float *src, float *dst)
v1 in %xmm0, src in %rdi, dst in %rsi
  vmovaps %xmm0, %xmm1      # 複製 v1(暫存器間)
  vmovss  (%rdi), %xmm0     # 讀 v2 = *src(記憶體→XMM)
  vmovss  %xmm1, (%rsi)     # 寫 *dst = v1(XMM→記憶體)
  ret                       # v2 由 %xmm0 回傳

FP → 整數:雙運算元轉換 (Figure 3.47)

指令 Source Destination 說明
vcvttss2si X/M32 R32 single → int(截斷)
vcvttsd2si X/M64 R32 double → int(截斷)
vcvttss2siq X/M32 R64 single → long(截斷)
vcvttsd2siq X/M64 R64 double → long(截斷)

整數 → FP:三運算元轉換 (Figure 3.48)

指令 Source 1 Source 2 Destination 說明
vcvtsi2ss M32/R32 X X int → single
vcvtsi2sd M32/R32 X X int → double
vcvtsi2ssq M64/R64 X X long → single
vcvtsi2sdq M64/R64 X X long → double

FP 格式互轉:gcc 的奇特慣用碼 (p.334-335)

gcc 不用單一指令 vcvtss2sd/vcvtsd2ss,而是產生兩道指令的組合:

single → double:
  vunpcklps %xmm0, %xmm0, %xmm0   # [x3,x2,x1,x0] → [x1,x1,x0,x0](複製低位元素)
  vcvtps2pd %xmm0, %xmm0          # 低 2 個 single → 2 個 double: [dx0, dx0]

double → single:
  vmovddup   %xmm0, %xmm0         # [x1,x0] → [x0,x0](複製低位元素)
  vcvtpd2psx %xmm0, %xmm0         # 2 個 double → 2 個 single,高半部補 0:
                                  # [0.0, 0.0, x0, x0](0.0 的 bit pattern 全 0)

範例 fcvt(p.335)綜合展示各種轉換:指標與整數參數走通用暫存器(i→%edi, fp→%rsi, dp→%rdx, lp→%rcx),double 回傳值在 %xmm0;(long)dvcvttsd2siq(可直接以記憶體 (%rdx) 為來源)、(float)ivcvtsi2ss(double)lvcvtsi2sdq(double)f 用 vunpcklps+vcvtps2pd 兩道指令。Practice Problem 3.50/3.51(p.336-337)即測驗這些指令與型別的對應,如 long→double 對應 vcvtsi2sdq %rdi, %xmm0, %xmm0


3.11.2 程序中的浮點碼 (p.337-338)

x86-64 以 XMM 暫存器傳遞浮點參數與回傳浮點值,慣例如下:

混合參數的分派規則:指標與整數走通用暫存器(%rdi, %rsi, %rdx, %rcx, %r8, %r9 依序),浮點值走 XMM(%xmm0, %xmm1, ... 依序),兩組計數各自獨立。因此暫存器分派同時取決於參數的「型別」與「同型別內的順序」,與參數在宣告中的整體位置無關。

範例(p.337):

宣告 分派結果
double f1(int x, double y, long z) x→%edi, y→%xmm0, z→%rsi
double f2(double y, int x, long z) 與 f1 完全相同(順序調換不影響)
double f1(float x, double *y, long *z) x→%xmm0, y→%rdi, z→%rsi(指標算整數類)
與整數慣例的差異(見 03-Machine-Level-Programs/04-Procedures-and-the-Stack):通用暫存器有 6 個參數名額且有 callee-saved 集合(%rbx, %rbp, %r12%r15);XMM 有 8 個參數名額但沒有任何 callee-saved——跨函式呼叫要保留浮點值,只能先存到 stack 或記憶體。

3.11.3 浮點算術運算 (p.338-340)

Scalar AVX2 算術指令 (Figure 3.49):有一個 (S1) 或兩個 (S1, S2) 來源運算元與一個目的 D。**S1 可為 XMM 或記憶體;S2 與 D 必須是 XMM。**每種運算都有 single(...ss)與 double(...sd)兩個版本:

Single Double 效果 說明
vaddss vaddsd D ← S2 + S1 浮點加
vsubss vsubsd D ← S2 − S1 浮點減(注意順序)
vmulss vmulsd D ← S2 × S1 浮點乘
vdivss vdivsd D ← S2 / S1 浮點除(注意順序)
vmaxss vmaxsd D ← max(S2, S1) 浮點最大值
vminss vminsd D ← min(S2, S1) 浮點最小值
sqrtss sqrtsd D ← √S1 浮點平方根(單一來源)
ATT 格式的減法與除法是 S2 − S1S2 / S1(第二運算元減/除以第一運算元)。例如 vsubsd %xmm2, %xmm0, %xmm0%xmm0 ← %xmm0 − %xmm2。由 gcc 輸出反推 C 運算式時最容易在此弄反。

範例 funct(p.338-339)展示混合型別運算 a*x - b/i:

double funct(double a, float x, double b, int i)
a in %xmm0, x in %xmm1, b in %xmm2, i in %edi
  vunpcklps %xmm1, %xmm1, %xmm1   # ┐ x: float → double
  vcvtps2pd %xmm1, %xmm1          # ┘ (標準兩指令序列)
  vmulsd    %xmm0, %xmm1, %xmm0   # a * x
  vcvtsi2sd %edi, %xmm1, %xmm1    # i → double
  vdivsd    %xmm1, %xmm2, %xmm2   # b / i
  vsubsd    %xmm2, %xmm0, %xmm0   # a*x − b/i
  ret                             # 結果在 %xmm0

三個浮點參數 a, x, b 依序在 %xmm0%xmm2,整數參數 i 在 %edi——再次印證 3.11.2 的分派規則。Practice Problems 3.53/3.54(p.339-340)即由此類程式碼反推參數型別或還原 C 程式。


3.11.4 浮點常數的定義與使用 (p.340)

AVX 浮點運算不能使用 immediate(立即值)運算元(與整數算術不同)。編譯器必須為常數配置並初始化記憶體儲存空間,程式再從記憶體讀值:

double cel2fahr(double temp) { return 1.8 * temp + 32.0; }

cel2fahr:
  vmulsd .LC2(%rip), %xmm0, %xmm0   # × 1.8(PC-relative 讀常數)
  vaddsd .LC3(%rip), %xmm0, %xmm0   # + 32.0
  ret
.LC2:
  .long 3435973837    # 0xcccccccd  1.8 的低位 4 bytes
  .long 1073532108    # 0x3ffccccc  1.8 的高位 4 bytes
.LC3:
  .long 0             # 32.0 的低位 4 bytes
  .long 1077936128    # 0x40400000  32.0 的高位 4 bytes

解碼 .LC2 為 1.8 的步驟(套用 02-Information-Representation/04-Floating-Point 的 IEEE 754 公式 V=(1)s×M×2E;normalized 時 E=eBiasBias=2k11(double 為 1023)、M=1+f):


3.11.5 浮點碼中的位元運算 (p.341-342)

gcc 有時在 XMM 暫存器上用位元運算實作有用的浮點效果 (Figure 3.50)。這些指令都是 packed 運算——把位元運算套用到兩個 source 的全部 128 bit 並更新整個 destination;對 scalar 資料我們只關心低 4 或 8 bytes 的效果:

Single Double 效果 說明
vxorps xorpd D ← S2 ^ S1 位元 exclusive-or
vandps andpd D ← S2 & S1 位元 and

利用 IEEE 754 的 bit-level 性質可組出常用操作(即 Practice Problem 3.56 的三個 pattern):

位元操作 遮罩(64-bit double) 浮點效果
vandpd 遮罩 0x7fffffffffffffff 清除 sign bit 絕對值 fabs(x)
vxorpd %xmm0,%xmm0,%xmm0(自己 xor 自己) 全零 產生 0.0(0.0 的 bit pattern 全為 0)
vxorpd 遮罩 0x8000000000000000 翻轉 sign bit 取負 -x
vxorps %xmm1, %xmm1, %xmm1 是 gcc 產生浮點 0.0 的慣用手法(免載入記憶體常數),Figure 3.51 的 find_range 中也出現。判讀這類題目時,先把 .long 十進位值轉成十六進位、依 little-endian 拼出遮罩,再看它動到哪些位元。

3.11.6 浮點比較運算 (p.342-344)

指令 依據 說明
ucomiss S1, S2 S2 − S1 比較 single precision
ucomisd S1, S2 S2 − S1 比較 double precision

PF (parity flag):整數運算中表示結果最低 byte 有偶數個 1(gcc 產生的整數碼很少用到,故 3.6.1 未介紹);浮點比較中則表示任一運算元為 NaN(unordered)。C 語言慣例:任何含 NaN 的比較一律視為失敗——連 x == x 在 x 為 NaN 時都得到 0。常用 jp(jump on parity)在比較結果 unordered 時跳轉。

Ordering S2:S1 CF ZF PF
Unordered(任一為 NaN) 1 1 1
S2 < S1 1 0 0
S2 = S1 0 1 0
S2 > S1 0 0 0

除 unordered 外,CF/ZF 的意義與 unsigned 比較相同(ZF:相等;CF:S2 < S1),因此後續搭配 jajbjbeunsigned 系跳躍指令(而非 signed 系的 jg/jl)。

範例:find_range 的控制流 (Figure 3.51, p.343-344)

將 float x 相對 0.0 分類,回傳 enum(C 的 enum 編碼為整數):NEG=0、ZERO=1、POS=2、OTHER=3(x 為 NaN 時):

        vxorps %xmm1,%xmm1,%xmm1      ; %xmm1 = 0.0
        vucomiss %xmm0, %xmm1         ; 比較 0 : x
        ja  ────────────────────┐     ; 0 > x → neg
        vucomiss %xmm1, %xmm0   │     ; 比較 x : 0
        jp  ──────────┐         │     ; NaN → posornan
        movl $1,%eax  │         │     ; 先設 result = ZERO
        je  ──────────┼───────┐ │     ; x == 0 → done
 .L8: <───────────────┘       │ │     ; posornan(x > 0 或 NaN)
        vucomiss .LC0(%rip),%xmm0     ; 比較 x : 0
        setbe %al                     ; NaN: CF=ZF=1 → 1;x>0 → 0
        movzbl %al,%eax       │ │
        addl $2,%eax          │ │     ; POS=0+2=2,OTHER=1+2=3
        ret                   │ │
 .L5: <───────────────────────┼─┘     ; neg
        movl $0,%eax          │       ; result = NEG
 .L3: <───────────────────────┘       ; done
        rep; ret

四種執行路徑:

書中指出此碼效率不佳:x 與 0.0 比較了三次(其實一次即可得到全部資訊),且 0.0 產生兩次(一次 vxorps、一次讀記憶體 .LC0)。


3.11.7 浮點碼觀察 (p.345)

3.12 本章總結 (p.345-346)


Exam/Test Patterns

情境 / 關鍵字 答案
浮點參數放哪個暫存器? 依「浮點參數的順序」放 %xmm0%xmm7(最多 8 個),與整數/指標參數(%rdi, %rsi, ...)分開計數;超過的走 stack
浮點回傳值在哪? %xmm0
XMM 有 callee-saved 嗎? 沒有,全部 caller-saved;callee 可任意覆寫
(int)double_val 對應指令? vcvttsd2si(截斷,round toward zero);轉 long 用 vcvttsd2siq
(double)long_val 對應指令? vcvtsi2sdq src, %xmmN, %xmmN(三運算元;第二 source 只影響高位 bytes,可忽略)
看到 vunpcklps + vcvtps2pd 連續兩道 gcc 的 float → double 慣用碼(等效單指令 vcvtss2sd)
看到 vmovddup + vcvtpd2psx gcc 的 double → float 慣用碼(等效 vcvtsd2ss),高半部補 0
vmovaps 讀寫記憶體時觸發 exception 位址未滿足 16-byte 對齊;vmovss/vmovsd 則不要求對齊
vsubsd S1, S2, D 算什麼? D = S2 − S1(第二運算元減第一);vdivsd 同理 D = S2 / S1
浮點運算可以用 $立即值 嗎? 不行;常數必須放記憶體(.LC 標籤),以 label(%rip) PC-relative 讀取
給兩個 .long(低位、高位)求浮點常數 little-endian 拼出 64-bit pattern → 拆 s/e/f → E=e1023M=1+fV=(1)sM2E
vxorpd %xmm0,%xmm0,%xmm0 產生 0.0(全零 bit pattern)
vandpd 遮罩 0x7fff...f / vxorpd 遮罩 0x8000...0 fabs(x)(清 sign bit)/ -x(翻 sign bit)
ucomisd 後接 jp 偵測 NaN(unordered 時 PF=1);NaN 使任何 C 比較失敗,含 x == x
ucomisd 後為何用 ja/jb 而非 jg/jl? 浮點比較設定 CF/ZF 的方式與 unsigned 比較相同
unordered 時 CF/ZF/PF 值? 全為 1(CF=1, ZF=1, PF=1);故 setbe(條件 CF∨ZF)在 NaN 時得 1
由 typedef 參數 + 組譯碼反推型別(PP 3.53 型) 看轉換指令與精度後綴:vcvtsi2ssq %rsi → 該參數是 long;vaddss → float 精度運算;直接以 %xmm 參與 ...sd 運算且無轉換 → double