iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

在學習 C++ STL 時不能只知道 container 如何使用,更要理解 abstraction 背後的 runtime cost。例如 vectorlistmapunordered_map 不僅僅是資料結構的不同,更包含了以下差異:

  • Memory Layout。
  • Cache Locality。
  • Memory Allocation。
  • Pointer Indirection。
  • Branch Prediction。
  • Latency Profile。

一個好的低延遲系統,並不能滿足於 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。實際成本可能包含:

  • Hash function
  • Bucket lookup
  • Collision handling
  • Pointer indirection
  • Cache miss
  • Dynamic allocation
  • Rehash

當 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 關心的是:

  • Who owns the resource?
  • When is it released?
  • What happens when scope ends?

STL 關心的是:

  • How is data represented?
  • How is it accessed?
  • Which abstraction fits the workload?

而 Low Latency 更進一步問:

  • How much does it cost?
  • When does it cost?
  • Is the cost predictable?
  • What happens in the worst case?

為了達到低延遲的目標,在每次使用 STL 時都應該思考下列幾個問題:

  • 一個 push_back() 什麼時候會 allocation?
  • 一次 unordered_map::find() 需要多少次 memory access?
  • 一個 container traversal 是否具有良好的 cache locality?
  • 某個 operation 是否可能觸發 rehash?
  • 某個 allocation 是否會落在 latency-critical path?

這些問題會讓 C++ 從單純的「語法與 API 集合」,逐漸變成一門關於 memory、object lifetime、CPU architecture、predictable performance 的語言。若能善用 std::unique_ptr<T> 等智能指標,更是掌握了 object ownership 與 lifetime management。


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

尚未有邦友留言

立即登入留言