iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0

上一篇我們以 STL 為基點,探討 memory layout、cache locality、allocation 及 tail latency,並拆解 Low-Latency C++ 中「資料結構如何影響實際執行成本」。複習一個重要觀念:Big-O 只是描述 complexity 的其中一個面向,實際的 latency 還取決於 CPU 實際需要執行什麼。

因此,在 latency-sensitive system 中,需要關注及思考的是:

  • 這個 operation 是不是 O(1)?
  • 這個 container 是否 cache-friendly?
  • 這個 operation 是否可能 allocation?
  • 這個 cost 什麼時候發生?
  • 這個 abstraction 本身是否會在 runtime 產生額外成本?

若一個 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 行為,以及高效率的程式碼生成。


上一篇
[Day 3] Advanced Modern C++ Foundations: STL
下一篇
[Day 5] Advanced Modern C++ Foundations: Abstraction II
系列文
從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言