浮點程式碼 (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 |
| 算術指令 | vaddsd、vsubsd、vmulsd、vdivsd、vmaxsd、vminsd、sqrtsd 等 |
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) 定義了浮點程式如何映射到機器,包含四個面向:
- 浮點值如何儲存與存取(通常透過某種暫存器)
- 操作浮點資料的指令
- 參數傳遞與回傳慣例
- 函式呼叫時暫存器的保存慣例(caller-saved / callee-saved)
歷史演進:自 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)
- 自 SSE2 起,媒體指令支援 scalar(純量)浮點運算——只用 XMM/YMM 暫存器的低 32 或 64 bit 存單一值,型態上更接近其他處理器的浮點支援。
- 所有能執行 x86-64 的處理器都支援 SSE2 以上,因此 x86-64 浮點以 SSE/AVX 為基礎(含參數傳遞與回傳的 ABI 慣例)。
- 本節以 AVX2(AVX 第二版)為準,gcc 加
-mavx2即產生;SSE 各版與 AVX1 概念相同,僅指令名稱與格式不同。書中只呈現編譯浮點程式時會出現的(多為 scalar 的)AVX 指令。
媒體暫存器配置 (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 記憶體範圍)
- 參照記憶體者為 scalar 指令(操作單一值而非 packed 資料);記憶體定址方式與整數
mov完全相同(displacement + base + index × scale 全組合)。 vmovss/vmovsd不要求對齊也能正確執行(但最佳化指南建議 32-bit 資料 4-byte 對齊、64-bit 資料 8-byte 對齊)。- gcc 慣例:記憶體↔XMM 用 scalar 指令;XMM↔XMM 之間用
vmovaps(single)/vmovapd(double) 複製整個暫存器——複製全部或只複製低位值對功能與速度皆無差別。
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(截斷) |
- 來源為 XMM 或記憶體,目的為通用暫存器(
%rax、%ebx等)。 - 一律執行截斷 (truncation)——向零捨入 (round toward zero),符合 C 與多數語言的規定;指令名中
tt的第二個t即 truncation。
整數 → 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 |
- 少見的三運算元格式(兩個 source、一個 destination):第一 source 來自記憶體或通用暫存器;第二 source 只影響結果的高位 bytes,對低位結果無影響,分析時可忽略;destination 必須是 XMM。
- 常見用法是第二 source 與 destination 相同,如
vcvtsi2sdq %rax, %xmm1, %xmm1(把%rax的 long 轉為 double 存入%xmm1低 8 bytes)。
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)
vunpcklps原用途是交錯 (interleave) 兩個 XMM 的字組:sources[s3,s2,s1,s0]、[d3,d2,d1,d0]→ destination[s1,d1,s0,d0];三個運算元同一暫存器時效果變成複製低位元素。- 書中指出這種寫法沒有明顯效益也無必要(值被重複存放),單指令
vcvtsd2ss %xmm0, %xmm0, %xmm0即可,gcc 為何如此產生原因不明——但讀 gcc 輸出時必須認得這兩個 pattern。
範例 fcvt(p.335)綜合展示各種轉換:指標與整數參數走通用暫存器(i→%edi, fp→%rsi, dp→%rdx, lp→%rcx),double 回傳值在 %xmm0;(long)d 用 vcvttsd2siq(可直接以記憶體 (%rdx) 為來源)、(float)i 用 vcvtsi2ss、(double)l 用 vcvtsi2sdq、(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 暫存器傳遞浮點參數與回傳浮點值,慣例如下:
- 最多 8 個浮點參數依序放入
%xmm0–%xmm7;更多的浮點參數改走 stack。 - 回傳浮點值的函式以
%xmm0回傳。 - 所有 XMM 暫存器都是 caller-saved——callee 可任意覆寫而不必先保存。
%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(指標算整數類) |
%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 |
浮點平方根(單一來源) |
S2 − S1、S2 / 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 公式
- 機器為 little-endian:第一個
.long是低位、第二個是高位 → 完整 bit pattern0x3ffccccccccccccd。 - 高位取出 exponent 欄位
e = 0x3ff = 1023→。 - 串接兩值的 fraction bits 得
0xccccccccccccd,即 0.8 的二進位小數表示;加上隱含的 leading 1 →,故值 。 - 同法可驗證
.LC3:0x4040000000000000→, , , → (Practice Problem 3.55)。
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 |
- 與
cmp系列類似(見 03-Machine-Level-Programs/03-Control-Flow),遵循 ATT 格式反序列運算元;S2 必須在 XMM,S1 可在 XMM 或記憶體。 - 設定三個 condition codes:ZF(zero flag)、CF(carry flag)、PF(parity flag)。
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),因此後續搭配 ja、jb、jbe 等 unsigned 系跳躍指令(而非 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:第 4 行
ja成立(0 > x 時 CF=ZF=0)→ 回傳 0。 - x = 0.0:
ja、jp皆不成立,je成立 →%eax= 1。 - x > 0.0:三個跳躍都不成立,
setbe得 0,addl $2→ 回傳 2。 - x = NaN:
jp成立跳 .L8;第三次vucomiss使 CF=ZF=1,setbe得 1,加 2 → 回傳 3。
書中指出此碼效率不佳:x 與 0.0 比較了三次(其實一次即可得到全部資訊),且 0.0 產生兩次(一次 vxorps、一次讀記憶體 .LC0)。
3.11.7 浮點碼觀察 (p.345)
- AVX2 浮點機器碼的整體風格與整數碼相似:都以一組暫存器保存與運算資料值,並用這些暫存器傳遞函式參數。
- 複雜之處在於各種資料型別、以及混合型別運算式的求值規則,使 AVX2 碼的指令種類與格式遠多於純整數函式。
- AVX2 具備以 packed data 平行運算加速的潛力;編譯器的自動向量化仍在發展中,目前最可靠的高效能做法是使用 gcc 對 C 語言的向量擴充(Web Aside opt:simd),與 05-Program-Optimization/03-Loop-Unrolling-and-Parallelism 的平行化主題相呼應。
3.12 本章總結 (p.345-346)
- 透過讓編譯器產生組合碼,我們同時洞察編譯器與其最佳化能力,以及機器、資料型別與指令集(第 5 章將利用此知識寫出映射更高效的程式);也更完整理解資料存放區域——run-time stack、動態配置結構、全域資料(第 12 章會用到)。
- 機器層級程式與 C 的差異:型別區分極少;程式是逐一執行單一操作的指令序列;暫存器與 run-time stack 等程式狀態直接可見;只提供低階操作——條件、迴圈、程序等控制結構須由編譯器以多道指令組合實作。
- C 缺乏邊界檢查 (bounds checking) 使許多程式易受 buffer overflow 攻擊,曾使許多系統暴露於惡意入侵;近年 run-time 系統與編譯器的防護(見 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety)已提升安全性。
- 其他語言/機器組合的處理方式大多類似:C++ 編譯與 C 非常接近(早期實作先做 C++→C 的 source-to-source 轉換再用 C 編譯器;物件以類似 C struct 的結構表示,method 以指向實作程式碼的指標表示)。Java 則完全不同:編成 Java byte code——一種虛擬機的機器層級程式,由軟體直譯器模擬執行,或以 just-in-time compilation 將 byte code 動態翻譯成機器指令(重複執行如迴圈時較快);好處是同一份碼可在許多不同機器上「執行」,而 x86-64 機器碼只能在 x86-64 上跑。
- Bibliographic Notes:Intel/AMD 官方指令文件全用 Intel 格式(與本書 ATT 格式的運算元順序相反);x86-64.org 負責定義 Linux 上 x86-64 的 ABI(application binary interface),規範 procedure linkage、binary code 檔等執行必需的細節。
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 → |
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 |
Related Notes
- 02-Information-Representation/04-Floating-Point — IEEE 754 編碼(
、Bias、NaN)是解讀浮點常數與比較行為的基礎 - 03-Machine-Level-Programs/01-Program-Encodings-and-Data-Formats — ATT vs Intel 格式、資料型別與指令後綴慣例
- 03-Machine-Level-Programs/02-Data-Movement-and-Arithmetic — 整數版 mov 與算術指令,和 vmov / v-arith 系列對照
- 03-Machine-Level-Programs/03-Control-Flow — condition codes 與條件跳躍;浮點比較沿用同一機制再加 PF
- 03-Machine-Level-Programs/04-Procedures-and-the-Stack — 整數/指標參數的暫存器傳遞與 caller/callee-saved 慣例對照
- 05-Program-Optimization/03-Loop-Unrolling-and-Parallelism — SIMD / 向量化是 AVX packed 運算的效能延伸