在一般的 C++ 開發中,我們往往習慣從 source code 的角度思考問題:
std::vector、std::unordered_map 或其他 container 是否適合?inline?unique_ptr、reference 或 value?但在 Low-Latency C++ 的世界裡,這些問題通常只是第一層。因為真正決定 latency 的並不是 C++ source code 本身,而是 compiler 將其轉換成什麼樣的 machine code,以及 CPU 如何執行這些 machine code。
1. 從 C++ Source Code 走到 Machine Code
在 Low-Latency C++ 中,我們不能只看 source code,更要了解其底層的概念。以下會舉一些簡化後的範例,實際過程有哪些 load、store、register movement 以及最終的 instruction sequence,則取決於 compiler 的 code generation、optimization、target architecture 等因素。
例如 processor.process(order); 看起來只有短短一行 function call,但 compiler 最後可能產生情況 1:
load // 找到 processor
↓
load // 找到 order
↓
call // 跳去執行 process()
而若 process() 原本是:
void process(Order order) {
if (order.price > 100) {
order.total += 10;
}
}
由於 process() 很小,而且 compiler 在當前編譯條件下可以看到其 implementation,compiler 可能會將這個 function inline。也就是將其運算邏輯直接整合到 call site,而不再需要實際的 function call:
if (order.price > 100) {
order.total += 10;
}
接著 compiler 再做 optimization,流程可能接近情況 2:
load // 讀 price
↓
cmp // 比較
↓
branch // 根據結果決定是否要執行
↓
add // 計算
甚至當 compiler 能證明這個 function 實際上只是:
order.total = order.price + order.tax;
那表示 abstraction 可以在 compile time 被消除,情況 3 之下 CPU 可能將這段程式降低成非常少量的 machine instructions:
load // 讀 price
↓
add // 加上 tax
↓
store // 把結果寫回 total
上訴例子可以看出,source code 中存在的 abstraction boundary,不一定會存在於最終 machine code 中。而在 low-latency optimization 中,不應該把 inline 關鍵字當成效能保證。真正重要的是 compiler 最後產生了什麼 code,這也是為什麼理解 assembly、compiler optimization、branch prediction、cache hierarchy,對 Low-Latency C++ 的開發非常重要。
2. 一個更完整的 Mental Model
到這裡,我們可以建立一個比較完整的 Low-Latency C++ mental model,其中每一層都可能影響程式效能:
Algorithm
↓
Data Structure / Abstraction
↓
Memory Layout
↓
Allocation / Lifetime
↓
Compiler Optimization
↓
Generated Machine Code
↓
Instruction Set Architecture (ISA)
↓
CPU Microarchitecture
├── Pipeline
├── Branch Prediction
├── Out-of-Order Execution
├── Cache Hierarchy
└── Memory Ordering
↓
Observed Latency
↓
Latency Distribution
如同之前反覆強調的觀念,低延遲系統最關心的並不只是平均 latency,而是整個 latency distribution 甚至更高 percentile 的 tail latency。即使平均值看似非常漂亮,仍然要避免少數 request 因為 cache miss、branch misprediction、memory stall、context switch 或其他 microarchitectural event 而突然變慢。
當 algorithm、data structure、allocation、memory layout 等較高層次因素都已經被最佳化後,下一步就需要理解 CPU microarchitecture 如何執行 compiler 產生的 machine code。此時就會進入 Low-Latency C++ 的下一個階段 —— Branch Prediction、Instruction Pipeline、Out-of-Order Execution、CPU Cache Hierarchy 以及 Memory Ordering。