在真正追求 nanosecond-level latency 的 C++ 系統中,資料如何排列與存取就是效能的一大考量。上篇文章已經提到 Array of Structures (AoS) 及 Structure of Arrays (SoA) 的概念,這次我們就從這裡繼續延伸,進一步探討 Data-Oriented Design (DOD)。
比起單純從物件與抽象的角度設計程式,DOD 更關注的是 CPU 實際會如何處理這些資料。在對 latency 極度敏感的系統中,這些看似底層的細節,往往會最直接地反映在最終的執行表現上。
1. 低延遲系統中的實際考量
在 latency-sensitive C++ 中,我們通常會進一步區分 hot data 與 cold data:
struct Order {
int64_t price;
int64_t quantity;
uint64_t id;
// cold data
char trader_name[64];
char comment[128];
};
如果 matching engine 的 hot path 只需要 price、quantity、id,那麼把 trader_name 和 comment 放在同一個 cache footprint 中就是一種資源上的浪費。因此我們可以將資料結構拆解:
struct OrderHot {
int64_t price;
int64_t quantity;
uint64_t id;
};
struct OrderCold {
char trader_name[64];
char comment[128];
};
甚至進一步使用 SoA 讓 hot path 的 working set 盡可能小:
struct OrderBook {
std::vector<int64_t> price;
std::vector<int64_t> quantity;
std::vector<uint64_t> id;
};
每次一次 cache miss、TLB miss、page fault 或 memory contention,都可能造成 latency tail。
Low latency 不只是讓 CPU 跑得快,而是讓 CPU 更少等待 memory。SoA 的價值就在於,它可以讓 hot data 更緊密更 predictable,降低不必要的 memory traffic,並讓現代 CPU 的 cache hierarchy 與 SIMD capabilities 更有效率地工作。
2. AoSoA —— High-Performance Code 的折衷方案
SoA 非常適合 SIMD。但如果資料量很大,或 algorithm 同時處理一小批 order,純 SoA 未必是最佳選擇。這時就可以使用 Array of Structures of Arrays (AoSoA):
Block 0
├── price[0..7]
└── quantity[0..7]
Block 1
├── price[0..7]
└── quantity[0..7]
Block 2
├── price[0..7]
└── quantity[0..7]
這在 SIMD-heavy、GPU、physics、simulation、HPC 系統中特別值得研究,能同時獲得多元的好處:
3. False Sharing —— DOD 不只是在解決 Cache Miss
struct Counters {
std::atomic<uint64_t> producer;
std::atomic<uint64_t> consumer;
};
若是兩個 thread 各自操作自己的變數,則不會有太大的問題。但如果兩個變數位於同一個 cache line,可能會導致 cache line 在 cores 之間不停 bouncing:
┌──────────────────────────────┐
│ producer │ consumer │
└──────────────────────────────┘
Thread A 修改 producer:
Core 0
↓
invalidate cache line
Thread B 又修改 consumer:
Core 1
↓
invalidate cache line
不過 False sharing 可以透過 padding / alignment 避免:
struct alignas(64) Counter {
std::atomic<uint64_t> value;
};
變成:
Core 0
┌──────────────────────────────┐
│ producer │
└──────────────────────────────┘
Core 1
┌──────────────────────────────┐
│ consumer │
└──────────────────────────────┘
所以 DOD 不只是讓資料靠近,有時候反而是讓不同 thread 修改的資料彼此遠離。具體該如何設計一個合適的資料結構,可以觀察下列項目再做決定: