Skip to content

重要专有名词解析 (Glossary)

在深入研究各种开源或商业的 ECS 框架之前,开发者往往会被大量的底层专有名词所劝退。由于 ECS 本质上是针对内存和 CPU 硬件的极度优化范式,许多在此领域使用的术语对于习惯了高级语言抽象的开发者来说是较少接触的。

为了让你在后续学习和阅读框架文档(如 EnTT, Flecs, Unity DOTS)时通畅无阻,本章将集中对几个最重要且容易混淆的专有概念进行通俗化解读。

1. 托管内存 (Managed Memory) 与非托管内存 (Unmanaged Memory)

Section titled “1. 托管内存 (Managed Memory) 与非托管内存 (Unmanaged Memory)”

在 C#(如 Unity)或 Java 等语言的生态中,这两个概念非常关键。

托管内存是指由垃圾回收器(Garbage Collector, GC)自动负责分配和释放的内存堆。当我们使用 new Object() 时,数据就分配在这里。托管堆的最大问题在于:如果不断产生临时对象,GC 就会被迫定期启动,在扫描和回收内存时会引起游戏的主线程卡顿(GC Spike)。此外,托管内存中的对象排布往往是不连续的。

非托管内存则直接向操作系统(如 C 语言中的 malloc)申请一块原生(Native)内存区域。这块内存不受 GC 管辖,没有自动释放机制,必须由开发者手动管理(分配与释放)。非托管内存通常能够实现严格的连续对齐存储,并且可以直接向外传递指针。

应用场景:为了追求极致性能和消除 GC 卡顿,像 Unity DOTS 这样的高性能 ECS 框架,要求所有组件数据必须声明为能在非托管内存中安全分配的极简结构(如 C# 中的 unmanaged struct)。

我们在第一章曾讨论过缓存穿透,这里给出正式定义。

CPU 缓存(L1, L2, L3)在与主存(RAM)进行数据交换时,并不是一个字节一个字节传输的,而是以一个固定大小的块为一个单位,这个单位就被称为 Cache Line(通常是 64 字节)。

这意味着,如果 CPU 想要读取内存地址 0x00 处的一个 4 字节整数,它会顺便把 0x000x3F(共 64 个字节)的所有数据一口气打包读取进 L1 缓存。这也是为什么 ECS 强调把同类型数据紧凑挨着分配存放在内存中的根本物理原因:读取一次,接下来几十次甚至上百次循环的数据都全在缓存里了,不需要再耗时去主存抓取。

这是导致缓存未命中(Cache Miss)的核心罪魁祸首。

如果你的数据结构是基于节点的(例如链表 LinkedList 或对象数组 Player[]),当你从节点 A 想跳转到节点 B 时,CPU 必须读取节点 A 中存储的指针地址,然后才能知道节点 B 在物理内存堆的什么地方。

这种“沿着指针满主存找东西”的行为就是指针追逐。由于物理地址跳转毫无规律,每一次指针追踪大概率都会导致前文提到的 CPU 必须重构一次 Cache Line(引发高昂的时间停顿)。ECS 架构的一大核心诉求就是彻底消灭核心循环中的指针追逐

批处理(Batching)是指不再写形如 Update(entity) 这种处理单个实体的函数,而是写形如 Update(Array<Entity>) 这样一次性处理连续几千个数据的逻辑管线。

批处理是触发硬件加速的前提条件之一,尤其是 SIMD(Single Instruction, Multiple Data,单指令多数据流)。 现代 CPU 中存在特殊的寄存器和指令集(如 AVX, SSE)。利用 SIMD,CPU 可以在耗费相同一根时钟周期的代价下,不再是仅仅算出一个 x1 + y1,而是能同时算出四个数对: [x1+y1, x2+y2, x3+y3, x4+y4]

只有当你在 ECS 等架构下,做到了数据完全连续对齐的批处理时,现代的编译器(甚至是特定引擎的编译器如 Unity 的 Burst Compiler)才能自动将你的循环优化转化为强大的 SIMD 机器指令。

在深入接下来的几章前,我们需要分清楚数据的两种经典底层排布模型,它们分别是:

  • AoS (Array of Structures,结构体数组):面向对象的常见排布模式。
  • SoA (Structure of Arrays,数组结构体):面向数据设计的核心排布模式。

这两个缩写是理解 ECS 内存管理机制的“金钥匙”。所有的 ECS 框架内部,无非是在这两种模型中做变种设计。我们将在下一节《内存布局对比:AoS 与 SoA》中深入拆解这两个模型在汇编和缓存层面的具体差异。


了解了这些基础的底层专属名词后,我们将可以更加顺畅地剖析那些精密复杂的 ECS 内部存储结构。