效能工程

資料導向設計(SoA 與 AoS)

/ SoA: ess-oh-AY; AoS: ay-oh-ESS /

假設你有一百萬個遊戲角色,每個有位置、速度、名字、以及上百個其他欄位,而每一幀你只更新位置與速度。自然的物件導向佈局把每個角色存成一個結構、保留它們的一個陣列——但當你串流走過、只更新一百零二個欄位中的兩個時,硬體會把「其他」一百個欄位也一起拖著走,因為記憶體是以快取線為單位來的。資料導向設計從相反的前提出發:圍繞你的資料「實際上如何被大批存取」來設計佈局,而非圍繞整潔的物件。

這個核心選擇有兩個名字。陣列結構(array of structs, AoS)是熟悉的佈局:一個陣列,每個元素是一個完整結構,所以一個角色的各欄位坐在一起,陣列在記憶體中交錯著 欄位1,欄位2,…,欄位1,欄位2,…。結構陣列(struct of arrays, SoA)則為每個欄位各保留一個獨立陣列:所有位置在一個連續陣列、所有速度在另一個,依此類推。現在更新位置與速度的迴圈走過兩個緊湊連續的陣列,每條載入的快取線都是 100% 有用的資料,硬體預取器完美串流——而用 AoS 時每條載入的線大多是用不到的名字與其他欄位。SoA 也讓編譯器能向量化(用一條 SIMD 指令一次套用到許多位置上),因為它需要的值緊鄰排放。更廣的資料導向心態是:找出那個熱門的轉換、看清楚它究竟碰到哪些位元組、把記憶體佈局成讓那些位元組緊密且循序——藉由改善空間區域性,讓快取與預取器為你工作。

它之所以重要,是因為對於資料量大、大批處理的程式碼(模擬、ECS 遊戲引擎、欄式資料庫、數值核心),單單 SoA 對 AoS 的選擇,動不動就讓吞吐量改變好幾倍,而完全不改演算法。誠實的提醒:SoA 並非放諸四海皆準地更好。如果你通常一起碰一個物件的「所有」欄位(一次隨機存取一整筆紀錄),AoS 把那筆紀錄保持在一條快取線上而勝出,SoA 反而會把它散到許多陣列。SoA 也讓「一個物件」變得難以傳遞,並可能讓插入與刪除變複雜。所以規則不是「永遠用 SoA」——而是選擇符合你主要存取模式的佈局,並且量測,因為交叉點取決於哪些欄位很熱、以及你如何走訪它們。

AoS:struct P { float x,y,z; char name[64]; } a[N]; // 更新 x,y,z -> 每條線大多是 name(浪費) SoA:struct { float x[N], y[N], z[N]; char name[N][64]; } a; // 更新 x,y,z -> 100% 有用的線、可向量化

只更新 x,y,z:AoS 每個元素都把用不到的 name 拖過快取;SoA 把熱欄位緊湊排放,使載入的每個位元組都被用到。

當你橫跨許多物件串流少數欄位時 SoA 勝;當你一起碰一個物件的所有欄位時 AoS 勝。沒有哪個放諸四海皆快——選擇符合你熱門存取模式的佈局,並用量測確認。

又称
data-oriented designstruct of arrays vs array of structsDOD資料導向設計結構陣列與陣列結構