在學習 C++ STL 時不能只知道 container 如何使用,更要理解 abstraction 背後的 runtime cost。例如 vector、list、map、unordered_map 不僅僅是資料結構的不同,更包含了以下差異:
一個好的低延遲系統,並不能滿足於 Big-O complexity。這個 operation 實際會讓 CPU 做什麼?它的成本發生在什麼時候?而且是否 predictable?這些都是要考量到的。最典型的例子就是 std::vector<Order>與 std::list<Order> 都可以儲存一系列 Order,但 memory layout 完全不同。
1. vector v.s. list
vector 通常使用 contiguous memory:
[ A ][ B ][ C ][ D ][ E ]
而 list 則是 node-based,每個 node 可能位於完全不同的 memory address:
[ A ] → [ B ] → [ C ] → [ D ] → [ E ]
舉例來說:
for (const auto& order : orders) {
process(order);
}
如果 orders 是 vector,CPU 可以 sequentially 存取相鄰的 memory。Cache line 被載入後,附近的元素很可能也會被接下來使用,硬體 prefetcher 也更容易發揮作用。相反地,list 的 traversal 需要不斷 follow pointer:
[ node A ]
↓
[ node F ]
↓
[ node C ]
↓
[ node Z ]
CPU 必須先取得目前 node,才能知道下一個 node 的位置。這種 pointer chasing 容易造成 cache miss,甚至形成 memory dependency chain。這也是 Low Latency programming 中非常重要的一個觀念:Memory access pattern 往往比單純的 Big-O 更接近實際效能。
而 Cache Locality 是 STL 效能的重要因素。現代 CPU 並不是每次都直接從 DRAM 取得資料,越靠近 CPU,通常 latency 越低:
┌──┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌───┐
│CPU│→│Register │→|L1 Cache│→│L2 Cache│→│L3 Cache│→|DRAM|
└──┘ └────┘ └────┘ └────┘ └────┘ └───┘
⬆ 最快 ⬆ 最慢
因為 CPU 最終執行的不是 Big-O,而是 load、store、branch,以及大量的 memory access。在選擇 container 時,不應只在乎哪個 container 的 complexity 比較低,而是應該思考 workload 是什麼?資料如何排列?CPU 將如何存取這些資料?
2. unordered_map 真的是 O(1)?
std::unordered_map<Key, Value> 的 O(1) 並不是免費的。平均 lookup complexity 是 O(1),但這並不代表 lookup 就一定是 Low Latency。實際成本可能包含:
當 container 持續成長 map.emplace(key, value); 超過 load factor threshold 後,可能觸發 rehash。這會產生額外的 allocation 與 bucket restructuring。
原本看似:
lookup → fast
實際上某個 operation 可能突然變成:
allocation → rehash → 大量 memory access → 可能造成 cache disruption
對 latency-sensitive system 而言,問題不只是平均 latency,而是 tail latency。因此除了 average latency,真正重要的問題可能不是 99% 的 request 有多快,而是最慢的 0.1% 到底發生了什麼?
3. 低延遲設計的好幫手 —— reserve()
上一篇文提到 RAII 的程式範例 orders_.reserve(10000); 不只是單純的 optimization,其實是在做 latency planning。
一般情況下,vector 成長時可能需要:
allocate new memory → move/copy existing elements → release old memory
而 reserve() 可以把 allocation 提前到 initialization 或 setup phase。換句話說,它把 runtime unpredictable cost 轉換成 initialization predictable cost。也就是一直重複這個核心觀念:不是所有 expensive operation 都必須消滅,而是盡可能把它們移出 latency-critical path。
4. 從 STL 重新理解 Low-Latency C++
RAII 關心的是:
STL 關心的是:
而 Low Latency 更進一步問:
為了達到低延遲的目標,在每次使用 STL 時都應該思考下列幾個問題:
push_back() 什麼時候會 allocation?unordered_map::find() 需要多少次 memory access?這些問題會讓 C++ 從單純的「語法與 API 集合」,逐漸變成一門關於 memory、object lifetime、CPU architecture、predictable performance 的語言。若能善用 std::unique_ptr<T> 等智能指標,更是掌握了 object ownership 與 lifetime management。