資料導向設計導論

資料導向設計導論

資料導向設計透過優先考慮記憶體布局而非物件階層來最大化效能

資料導向設計(DOD)是一種軟體設計方法,將焦點從「物件」轉移到記憶體中資料的讀取與寫入方式。透過根據資料的存取模式而非其概念身份來組織資料,開發者可以大幅降低 CPU 快取未命中,並簡化多執行緒與硬體卸載(例如 GPU 或 APU)的實作。

效能差距:記憶體延遲 vs. CPU 速度

  • L1/L2 Cache: 1–2 cycles
  • Main Memory: Up to 600 cycles (at 3.2 GHz)

當資料在記憶體中分散時,CPU 大多時間都在等待資料到達,而不是進行計算。DOD 透過確保特定操作所需的資料在記憶體中連續存放,來讓 CPU 持續獲得資料。

物件導向設計 (OOD) 與資料導向設計 (DOD) 的比較

OOD 問題:快取未命中與未使用的資料

在物件導向設計中,資料通常被分組到類別中。例如,Bot 類別可能包含位置、修飾子和瞄準方向。當程式為 1,000 個 Bot 更新瞄準方向時,它會為每個 Bot 載入整個 Bot 物件到快取中。

這導致兩種主要低效率:

  1. 資料未命中: CPU 會載入整個物件,包括當前操作不需要的資料(例如,載入 Bot 的名稱或庫存,而只更新其 aimDirection)。
  2. 指令快取未命中: 對許多不同物件呼叫如 updateAim() 的方法會導致程式執行頻繁跳躍,造成 iCache 未命中。

DOD 解決方案:線性陣列與變換

DOD 將系統視為一系列的變換:資料輸入 → 變換 → 資料輸出。與其使用 Bot 物件,DOD 會將屬性存放在獨立的線性陣列中(即結構的陣列而非陣列的結構)。

範例:更新 Bot 目標

  • OOD 方法: 遍歷 Bot 物件陣列並呼叫 bot.updateAim()
  • DOD 方法: 將線性 positions 陣列、線性 modifiers 陣列以及目標目的地傳遞給一個函式。該函式線性遍歷這些陣列,並將結果寫入線性 aimDirections 陣列。

這確保了載入 CPU 快取行(通常為 128 位元組)的每個位元組都用於當前計算,消除了浪費的記憶體頻寬並降低了延遲。

關鍵實作原則

設計 "Back to Front"

開發者應該先定義所需的輸出資料,然後決定產生該輸出所需的最少輸入資料。這可以防止在效能關鍵路徑中加入不必要的資料。

原生資料 vs. 來源資料

資料應該預先格式化以適應其運行的硬體。例如,來源資料可能會以鏈結リスト形式儲存以方便編輯,但原生運行時資料應該轉換為連續陣列以確保快取效率。

平行處理與硬體卸載

由於 DOD 將資料與程式碼分離並以線性方式組織,因此平行化變得容易許多。當資料已知且被隔離時,可以將其卸載到如 GPU 或 SPU 的協同單元,而無需複雜的鎖定,因為資料存取模式是可預測且自包含的。

關鍵觀點與權衡

  • 彈性 vs. 效能: 一些從業者主張,當需求頻繁變更時,DOD 比 OOD 少彈性。因為資料布局與特定演算法緊密耦合,變更「問題」可能需要完全重新設計資料結構。
  • 適用性: DOD 在處理大量平行資料的系統中最為有效,例如物理引擎、3D 渲染器和遊戲引擎。對於較小的資料集,管理獨立陣列的開銷可能會超過效能收益。
  • 與 ECS 的關係: 實體元件系統(ECS)常被用作實作 DOD 的框架。雖然不是每個問題的完美解決方案,但 ECS 通常比深層 OOD 階層更具彈性,以達到接近最佳的效能。

"如果你有不同的資料,你就有不同的問題。」 — 見解來自對 Mike Acton 方法的討論。

Sources