iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局系列 第 10

[Day 10] Hardware-Aware Memory & CPU Architecture: Data-Oriented Design II

  • 分享至 

  • xImage
  •  

在真正追求 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 只需要 pricequantityid,那麼把 trader_namecomment 放在同一個 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 系統中特別值得研究,能同時獲得多元的好處:

  • SoA 的 SIMD friendliness。
  • AoS 的 locality。
  • Predictable block size。

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 修改的資料彼此遠離。具體該如何設計一個合適的資料結構,可以觀察下列項目再做決定:

  • cache misses
  • branch misses
  • IPC
  • cycles
  • instructions
  • TLB misses
  • LLC misses
  • memory bandwidth
  • context switches
  • page faults

上一篇
[Day 9] Hardware-Aware Memory & CPU Architecture: Data-Oriented Design
下一篇
[Day 11] Hardware-Aware Memory & CPU Architecture: Data Ownership
系列文
從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言