上一篇我們以 STL 為基點,探討 memory layout、cache locality、allocation 及 tail latency,並拆解 Low-Latency C++ 中「資料結構如何影響實際執行成本」。複習一個重要觀念:Big-O 只是描述 complexity 的其中一個面向,實際的 latency 還取決於 CPU 實際需要執行什麼。
因此,在 latency-sensitive system 中,需要關注及思考的是:
若一個 abstraction 沒有被使用,就不應該為它付出成本;反之,也不該比直接撰寫等價功能的程式碼更昂貴。
1. Zero-Overhead Abstractions
在 C++ 中經常使用 abstraction 讓程式更容易維護。例如:
class OrderProcessor {
public:
virtual void process(const Order& order) = 0;
};
class BuyOrderProcessor : public OrderProcessor {
public:
void process(const Order& order) override {
// ...
}
};
這種設計使用的是 runtime polymorphism。
在一般情況下呼叫 processor->process(order);,compiler 需要透過 virtual dispatch 找到實際要執行的 function:processor → vptr → vtable → virtual function → process()
這並不代表 virtual function 一定很慢。現代 compiler 可以進行 devirtualization,在某些情況下甚至能把 virtual call 最終最佳化掉。但是在實務上的 latency-sensitive code path 中,不會希望把效能建立在「compiler 這一次能不能成功猜到 runtime type」的不確定性上。
尤其當 polymorphism 出現在 hot loop 裡,則最重要的不是 virtual function 的效能,而是確認 dynamic dispatch 是否有必要發生在 runtime。
for (const auto& order : orders) {
processor->process(order);
}
2. Runtime Polymorphism vs Static Polymorphism
如果在 compile time 就知道實際的 type,就可以利用 template 等技巧,把 runtime abstraction 移至 compile time:
template <typename Processor>
class OrderEngine {
public:
void run(const Order& order) {
processor.process(order);
}
private:
Processor processor;
};
class BuyOrderProcessor {
public:
void process(const Order& order) {
// ...
}
};
OrderEngine<BuyOrderProcessor> engine;
engine.run(order);
這時聰明的 compiler 知道 Processor = BuyOrderProcessor,因此 processor.process(order); 不需要 virtual dispatch。
更重要的是,compiler 還可以進一步 inline。概念上原本可能是:run() → 呼叫 process() → 執行 process() → return run()
最後可能直接變成:run() → 直接執行 process() 的實際內容
也就是說,compiler 可能將 process() 的內容直接嵌入 run(),讓原本的 function call 消失,並進一步對整段程式碼做最佳化。甚至 run() 本身也可能被 inline。 OrderEngine<BuyOrderProcessor> 這個 abstraction 仍然存在,只是它的成本被 compiler 在 compile time 消除了,這就是 static polymorphism。
3. 低延遲系統的大哉問
在設計一個 latency-sensitive component 時,可以用幾個要點來檢查 abstraction 但必要性。
若這個資訊並非 runtime 時期才獲得,就可以考慮在 compile time 使用以下語法:
template
constexpr
consteval
concept
if constexpr
這個 dispatch 是否需要 runtime?如果 processor type 在整個 application lifetime 都固定,那麼可以考慮使用 template;但如果 processor 真的是 runtime plugin,或 type 會在 runtime 改變,那麼 virtual dispatch 反而可能是合理設計。
Compile-time optimization 的收益,必須和 code footprint 一起看。需考量 compile-time specialization 是否會造成 code bloat,若產生大量的 code 可能會傷害 instruction cache。
最重要的是理解真正的 bottleneck 在哪裡。virtual call 不一定是問題, template 也不一定是最優解。應該做的其實是 measurement-driven engineering:
Measure → Profile → Identify hot path → Understand generated machine code
→ Optimize → Measure again
C++ 最有價值的地方之一,正是它可以同時擁有 abstraction 與 performance,讓 compiler 根據 compile-time information 產生高度特化的 machine code。而一個優質的程式架構,應該同時具備清楚的高層次設計、可預測的 runtime 行為,以及高效率的程式碼生成。