Skip to content

内存布局对比:AoS 与 SoA

在讨论数据驱动和 ECS 时,“内存布局(Memory Layout)”是一个无法避开的核心话题。我们如何将程序的属性数据放入主存,直接决定了 CPU 读取它的效率。

业内有两种最经典的内存排列模型:AoS(Array of Structures)SoA(Structure of Arrays)。本章将详细对比它们的差异,并解释为何 ECS 坚决拥抱 SoA 模型。

1. AoS:结构体数组 (Array of Structures)

Section titled “1. AoS:结构体数组 (Array of Structures)”

AoS 是我们在面向对象(OOP)编程中最直觉、最常见的写法。它指的是先定义一个包含多种属性的结构体(或实体类),然后创建一个由该结构体组成的数组。

假设我们有一个包含了位置坐标 Position 和颜色 Color 的粒子对象:

struct Particle {
public float px, py, pz; // 发送渲染需要的位置数据 (12 bytes)
public float r, g, b; // 发送渲染需要的颜色数据 (12 bytes)
}
// AoS 布局
Particle[] particles = new Particle[1000];

在物理内存中的排布: 当我们把这个数组放入内存时,数据是交错排列的(Interleaved)。每一个实体的所有属性紧紧挨在一起作为一个整体。

| px0, py0, pz0, r0, g0, b0 | px1, py1, pz1, r1, g1, b1 | px2, py2, pz2...

AoS 的优势:

  • 符合人类直觉:代码极易阅读,数据在逻辑上被绑定在一个叫“粒子”的实体概念中。
  • 随机修改单个实体极快:如果我只想修改粒子 0 的位置和颜色,它的所有数据都在相邻的内存中,只需要拉取一次缓存。

AoS 的致命缺陷: 如果我们需要执行一个批处理系统:只更新所有粒子的位置,不关心颜色。 当 CPU 读取 px0 时,由于 Cache Line(设为 64 字节)的存在,CPU 迫不得已将紧随其后的 r0, g0, b0 也打包塞进了极其宝贵的 L1 缓存中。

这就导致缓存行中,一半的空间存储了当前逻辑不需要的无用数据(颜色)。这意味着 CPU 本可以一次缓存 5 个以上粒子的位置来运算,现在却只能缓存 2 到 3 个。有效利用率直接腰斩,内存带宽被严重浪费。

2. SoA:数组结构体 (Structure of Arrays)

Section titled “2. SoA:数组结构体 (Structure of Arrays)”

SoA 则是面向数据设计(DOD)的信仰。它的作法是直接颠倒概念:不再以单个实体为单位声明数组,而是以单一属性为单位声明数组。最终通过相同游标(Index)将它们在逻辑上关联。

用 SoA 风格重写粒子系统:

struct ParticleSystemData {
public float[] px, py, pz;
public float[] r, g, b;
}
// SoA 布局
ParticleSystemData ps = new ParticleSystemData();
ps.px = new float[1000];
// ... 初始化其他数组

在物理内存中的排布: 现在,相同的属性在内存中是背靠背紧密贴合的,形成了一条条极其纯粹的数据流。

px 数组: | px0, px1, px2, px3, px4, px5, px6, px7 ... |
py 数组: | py0, py1, py2, py3, py4, py5, py6, py7 ... |
r 数组: | r0, r1, r2, r3, r4, r5, r6, r7 ... |

SoA 的优势:

  • 极致的缓存命中率:当我们再度执行那个“只更新所有粒子位置坐标”的批处理时。CPU 抓取 px0 所在的 Cache Line 时,塞入 L1 缓存的将是 [px0, px1, px2... px15] 这一整串纯洁无瑕的有效数据!没有任何颜色数据进来干扰。这正是 ECS 苦苦追求的完美硬件亲和性
  • 极为适合 SIMD 并行:现代编译器的 SIMD 指令需要数据类型单一且完全对齐。当你将如上连续的 px 数组推给编译器时,它可以轻易做到“单指令更新四个位移”。

SoA 的代价:

  • 代码编写极其反人类:手动编写和管理大量的零散数组(尤其是需要增删改实体时),会让代码丧失可读性,编写极度繁琐和容易出错。

面对这个抉择,ECS 架构展现出了它的天才之处:向外暴露 AoS 的开发体验,在底层封装 SoA(或者 AoSoA)的内存排布。

回顾我们在第二章讲的内容,开发者在写代码时定义的组件(Component),其实就是类似于 AoS 里那个小巧的结构体。开发者使用 AddComponent 或者 system.Query 时,也是在用面向实体的舒适语法。

但这一切只是 ECS 框架提供的上层幻象。在 ECS 框架的内存池管理中心,它默默地截获了你传入的零散组件数据,并将它们拆解、组装,严格按照 SoA 的格式,推入了那一条条连续且纯粹的底层大数组里。

如何巧妙地实现这套将抽象变成性能流的“魔法底层模型”呢?

这便引出了我们下一章的内容:当前主流 ECS 框架最核心的组件寻址模型——稀疏集 (Sparse Set)