你有没有过这样的体验:超市购物时,如果你要买十样东西,每样东西分散在不同货架的各自区域——面包在烘焙区、牛奶在冷藏区、零食在零食区——你得满场跑。但如果这十样东西恰好都在同一个货架上呢?一趟拿完,省时省力。
CPU 遍历数据也是一样的故事。
假设你有一组粒子,每个粒子有 x 坐标、y 坐标、速度和质量。非常直觉的写法是 Array of Structs:
struct Particle { float x, y, v, m; }
Particle particles[1024];
内存布局长这样:x₀ y₀ v₀ m₀ | x₁ y₁ v₁ m₁ | x₂ y₂ v₂ m₂ | ...
很自然对吧?每个粒子是一块完整的对象,读一个粒子就能拿到它的所有属性。
但如果你要遍历所有粒子的 x 坐标求质心呢?CPU 从内存读数据不是逐字节的,而是一整块一整块地搬——这个块叫 cache line,通常 64 字节。一个 Particle 是 16 字节,凑巧一个 cache line 能塞下 4 个。听起来不错?
问题在于:当你只要 x,cache line 却把 x、y、v、m 全搬了进来。你只用了 1/4 的数据,剩下 3/4 都是白搬的。你的带宽就这么浪费了 75%。
换成 Struct of Arrays 呢:
struct Particles { float x[1024], y[1024], v[1024], m[1024]; }
内存布局:x₀ x₁ x₂ x₃ | x₄ x₅ x₆ x₇ | ... | y₀ y₁ y₂ y₃ | ...
现在遍历 x 数组,一个 cache line 拿到的是 16 个连续的 x 值。没有 y、v、m 抢位子——每一字节都在为你干活。带宽利用率从 25% 直接拉到接近 100%。
实测下来,同样是遍历 100 万个粒子求 x 的总和,SoA 可以比 AoS 快 3-4 倍。这不是算法变刁了,纯粹是数据排得更对脾气,让缓存替你干活。
当然,SoA 不是万能药。如果你经常需要同时读写单个粒子的所有属性(比如物理碰撞检测),AoS 的局部性反而更好,因为一个粒子的数据都在一个 cache line 里。选哪种布局,取决于你的"主要访问模式"是什么——逐字段扫描,还是逐对象操作。
下次写热循环之前,先问自己:我的数据是按字段走的,还是按对象走的?把字段的呼吸对齐到 cache line 的节奏上,剩下的交给硬件就好。
#CS
CPU 遍历数据也是一样的故事。
假设你有一组粒子,每个粒子有 x 坐标、y 坐标、速度和质量。非常直觉的写法是 Array of Structs:
struct Particle { float x, y, v, m; }
Particle particles[1024];
内存布局长这样:x₀ y₀ v₀ m₀ | x₁ y₁ v₁ m₁ | x₂ y₂ v₂ m₂ | ...
很自然对吧?每个粒子是一块完整的对象,读一个粒子就能拿到它的所有属性。
但如果你要遍历所有粒子的 x 坐标求质心呢?CPU 从内存读数据不是逐字节的,而是一整块一整块地搬——这个块叫 cache line,通常 64 字节。一个 Particle 是 16 字节,凑巧一个 cache line 能塞下 4 个。听起来不错?
问题在于:当你只要 x,cache line 却把 x、y、v、m 全搬了进来。你只用了 1/4 的数据,剩下 3/4 都是白搬的。你的带宽就这么浪费了 75%。
换成 Struct of Arrays 呢:
struct Particles { float x[1024], y[1024], v[1024], m[1024]; }
内存布局:x₀ x₁ x₂ x₃ | x₄ x₅ x₆ x₇ | ... | y₀ y₁ y₂ y₃ | ...
现在遍历 x 数组,一个 cache line 拿到的是 16 个连续的 x 值。没有 y、v、m 抢位子——每一字节都在为你干活。带宽利用率从 25% 直接拉到接近 100%。
实测下来,同样是遍历 100 万个粒子求 x 的总和,SoA 可以比 AoS 快 3-4 倍。这不是算法变刁了,纯粹是数据排得更对脾气,让缓存替你干活。
当然,SoA 不是万能药。如果你经常需要同时读写单个粒子的所有属性(比如物理碰撞检测),AoS 的局部性反而更好,因为一个粒子的数据都在一个 cache line 里。选哪种布局,取决于你的"主要访问模式"是什么——逐字段扫描,还是逐对象操作。
下次写热循环之前,先问自己:我的数据是按字段走的,还是按对象走的?把字段的呼吸对齐到 cache line 的节奏上,剩下的交给硬件就好。
#CS