如何測量 x86-64 效能最佳化?
以 profiler 與 benchmark 驗證 uop、相依性、快取與分支預測
如何測量 x86-64 效能最佳化?
本文章以教學為目的。 效能會隨 microarchitecture、電力與溫度、記憶體階層、OS、輸入而變化,不要把固定 cycle 數當成普遍規則。
指令少不保證更快。一條指令可能拆成多個 uop,獨立指令可以重疊執行,而一次 cache miss 或長 dependency 就可能主宰整個迴圈。最佳化是量測瓶頸、建立因果假設,再用受控實驗嘗試反證的過程。
latency 與 throughput
latency 是結果可供相依指令使用前的延遲;throughput 是可開始獨立運算的頻率。
; dependency chain
add rax, rbx
add rax, rcx
add rax, rdx
; independent
add r8, rbx
add r9, rcx
add r10, rdx
同樣三個 add 有不同 critical path。benchmark 必須說明測的是哪一種。
多層瓶頸
| 層 | 例子 |
|---|---|
| Front-end | instruction cache、decode、uop cache |
| Execution | port、latency、dependency、divider |
| Memory | cache/DRAM/TLB、store forwarding |
| Control | branch misprediction、indirect target |
| System | page fault、scheduler、lock、I/O |
原始碼行數、指令數、binary bytes、jmp 數都只是線索,不是時間證明。先以 wall time 與 profiler 確認 hot path。
locality 與分支分布
連續陣列有利 cache line 與 prefetch。大結構只讀一個 field 或 pointer chasing 會浪費頻寬、限制 memory-level parallelism。AoS 改 SoA、縮小 working set 可能比 ASM 更有效。
幾乎永遠往同方向的分支很好預測。亂序輸入可能讓 cmov 或 mask 有利,但也可能執行兩邊工作、增加 dependency。與產品分布不同的隨機 benchmark 會誤導。
benchmark 契約
記錄假設、輸入大小與分布、避免 dead-code elimination、是否包含 setup、cold/warm cache、反覆是否獨立。再記錄 CPU、microcode、compiler/options、target ISA、LTO/PGO、seed、affinity、電源、溫度、重複次數、結果分布、OS 負載。看多次獨立 run,而非單一最好值。
rdtsc 不是自動的函式計時器;out-of-order、serialization、migration、invariant TSC 都必須處理。先使用可信 benchmark framework 與 monotonic clock。
profiler、counter 與 compiler
sampling profiler 找到時間位置;hardware counter 幫助檢查 cycles、branch、cache/TLB 假設。但 event 意義依 CPU 而異,也有 multiplexing、skid、speculation。IPC 一個數字不能判定好壞。
最佳化順序:建立正確的 C/C++ 基準與測試、查看 Release output、改善 algorithm/data layout、考慮 PGO/target/intrinsic,最後才在狹窄 hot loop 實驗 ASM。獨立 ASM 函式可能失去 inlining 與跨函式最佳化。
速度比較前必須測試 0、1、vector 邊界、大輸入、overflow、alignment、alias/overlap、FP 特殊值、feature fallback、sanitizer 與 unwind。
總結
x86-64 效能由 latency、throughput、uop、執行資源、dependency、cache/TLB、branch 預測與系統雜訊共同決定。以 profiler 找 hot path,用明確假設的 benchmark 驗證,只在效益與維護條件可重現的狹窄區域保留 handwritten ASM。