实体系统的缓存效率如何?


32

最近,我已经做了大量关于实体系统的阅读,以在我的C ++ / OpenGL游戏引擎中实现。我经常听到称赞的关于实体系统的两个主要好处是

  1. 由于不必纠缠复杂的继承层次结构,因此可以轻松地构造新类型的实体,并且
  2. 缓存效率,这让我很难理解。

当然,理论很简单;每个组件都连续存储在一块内存中,因此关心该组件的系统可以遍历整个列表,而不必在内存中跳转并杀死缓存。问题是我真的无法想到实际可行的情况。


首先,让我们看一下组件的存储方式以及它们之间的相互引用。系统需要能够使用多个组件,即渲染系统和物理系统都需要访问变换组件。我已经看到许多解决此问题的可能实现,但没有一个能很好地实现。

您可以让组件存储指向其他组件的指针,或者指向存储实体的指针的实体的指针。但是,一旦您将指针添加到组合中,您就已经失去了缓存效率。您可以确保每个组件数组都为'n'个大数组,其中'n'为系统中存在的实体数量,但是这种方法浪费内存。这使得很难向引擎中添加新的组件类型,但是仍然会浪费缓存效率,因为您要从一个数组跳到另一个数组。您可以交错实体数组,而不是保留单独的数组,但是您仍然在浪费内存。使得添加新组件或系统的成本高得惊人,但是现在有了使所有旧级别失效并保存文件的额外好处。

所有这些都假定在列表,每一帧或每笔价格变动中对实体进行线性处理。实际上,情况并非如此。假设您使用扇形/门户渲染器或八叉树来执行遮挡剔除。您也许可以在一个扇区/节点内连续存储实体,但是无论您是否喜欢它都将四处走动。然后,您将拥有其他系统,它们可能更喜欢以其他顺序存储的实体。在开始使用AI LOD之前,AI可以将实体存储在一个很大的列表中。然后,您需要根据与玩家之间的距离或其他一些LOD指标来拆分该列表。物理学将要使用该八叉树。脚本无关紧要,无论如何,它们都需要运行。

我可以看到在“逻辑”(例如ai,脚本等)和“世界”(例如渲染,物理,音频等)之间拆分组件,并分别管理每个列表,但是这些列表仍然必须彼此交互。如果AI不能影响用于实体渲染的变换或动画状态,则AI是没有意义的。


实体系统在现实游戏引擎中如何“高效缓存”?也许有一种混合方法,每个人都在使用而不是谈论,例如将实体全局存储在数组中并在八叉树中引用它?


请注意,如今您拥有多核CPU,并且缓存的容量大于一行。即使您需要来自两个系统的访问信息,它们也可能同时适用于两个系统。还要注意,图形渲染通常是分开的-完全按照您所说的(树,场景等)
wondra

2
实体系统并不总是具有高速缓存效率,但是它可以是某些实现的优势(相对于其他实现相似事物的方式)。
乔什

Answers:


43

我经常听到赞扬的关于实体系统的两个主要好处是:1)由于不必纠缠复杂的继承层次结构,因此可以轻松构造新型的实体,以及2)缓存效率。

请注意,(1)是基于组件的设计的好处,而不仅仅是ES / ECS。您可以通过许多没有“系统”部分的组件来使用它们,并且它们可以正常工作(并且很多独立游戏和AAA游戏都使用这种架构)。

标准的Unity对象模型(using GameObjectMonoBehaviourobjects)不是ECS,而是基于组件的设计。当然,较新的Unity ECS功能是实际的ECS。

系统需要能够使用多个组件,即渲染系统和物理系统都需要访问变换组件。

某些ECS按实体ID对它们的组件容器进行排序,这意味着每个组中的相应组件将以相同的顺序排列。

这意味着,如果要在图形组件上进行线性迭代,则还应在相应的转换组件上进行线性迭代。您可能正在跳过某些变换(因为您可能具有未渲染的物理触发体积等),但是由于您始终在内存中一直向前跳过(通常距离不是特别大),因此您仍会继续来提高效率。

这类似于将阵列结构(SOA)作为HPC的推荐方法。CPU和缓存可以处理多个线性阵列,几乎可以处理单个线性阵列,并且比处理随机内存访问要好得多。

在某些ECS实现中(包括Unity ECS)使用的另一种策略是根据组件的相应实体的原型来分配组件。也就是说,恰恰是一套组件的所有实体(PhysicsBodyTransform)将分别从不同组件的实体分配(例如PhysicsBodyTransform Renderable)。

这种设计中的系统通过首先找到满足其要求的所有原型(具有必需的组件集),迭代该原型列表以及迭代存储在每个匹配原型中的组件来工作。这允许在原型中进行完全线性和真实的O(1)组件访问,并允许系统以非常低的开销找到兼容的实体(通过搜索一小部分原型而不是搜索可能成千上万的实体)。

您可以让组件存储指向其他组件的指针,或者指向存储实体的指针的实体的指针。

引用同一实体上其他组件的组件不需要存储任何内容。要引用其他实体上的组件,只需存储实体ID。

如果单个实体允许一个组件存在多次,并且您需要引用一个特定实例,则存储该实体的另一个实体的ID和组件索引。但是,许多ECS实施不允许这种情况,特别是因为这会使这些操作效率降低。

您可以确保每个组件数组都为“ n”个大数组,其中“ n”为系统中存在的实体数量

使用句柄(例如索引+生成标记)而不是指针,然后可以调整数组的大小而不必担心破坏对象引用。

std::deque如果您出于某些原因要允许指针,或者您已经测量到问题,您也可以使用类似于许多常见实现的“块数组”方法(数组的数组)(尽管没有所述实现的小块大小)。数组大小调整性能。

其次,所有这些都假设实体在每个帧/刻度内线性地在列表中进行处理,但实际上,这种情况并不常见

它取决于实体。是的,在许多用例中,这是不正确的。的确,这就是为什么我如此强调基于组件的设计(好的)与实体系统(CBD的一种特定形式)之间的区别的原因。

您的某些组件肯定很容易进行线性处理。即使在通常的“树木繁茂”的使用案例中,我们也绝对可以看到,使用紧密排列的阵列可以提高性能(大多数情况下,涉及N最多为数百的情况,例如典型游戏中的AI代理)。

一些开发人员还发现,使用面向数据的线性分配数据结构的性能优势超过了使用“更智能”的基于树的结构的性能优势。当然,这完全取决于游戏和特定的用例。

假设您使用扇形/门户渲染器或八叉树执行遮挡剔除。您也许可以在一个扇区/节点内连续存储实体,但是无论您是否喜欢它都将四处走动。

您会惊讶于该阵列仍能提供多少帮助。与“任何位置”相比,您在较小的内存区域中跳跃,即使进行了所有跳跃,您仍然更有可能最终出现在缓存中。使用一棵特定大小或更小的树,您甚至可以将整个对象预取到缓存中,而不会在该树上出现缓存未命中的情况。

还存在一些树结构,它们以紧密排列的数组形式存在。例如,对于八叉树,您可以使用类似堆的结构(父级先于子级,兄弟姐妹彼此相邻),并确保即使“钻取”树时也始终在数组中向前迭代,这有助于CPU优化了内存访问/缓存查找。

这是很重要的一点。x86 CPU是一个复杂的野兽。CPU在您的机器代码上有效地运行了一个微代码优化器,将其分解为较小的微代码并重新排序指令,预测内存访问模式等。 CPU或缓存的工作方式。

然后,您将拥有其他系统,它们可能更喜欢以其他顺序存储的实体。

您可以多次存储它们。用这种方法将数组精简到最低限度的细节后,您可能会发现实际上节省了内存(因为您删除了64位指针并且可以使用较小的索引)。

您可以插入实体数组而不是保留单独的数组,但是仍然浪费内存

这与良好的缓存使用情况相反。如果您只关心变换和图形数据,为什么还要让机器花时间为物理和AI提取所有其他数据,以及输入和调试等等?

这就是通常倾向于ECS与整体游戏对象的观点(尽管与其他基于组件的体系结构相比并不适用)。

值得一提的是,据我所知,大多数“生产级” ECS实施都使用交错存储。我之前提到的流行原型方法(例如,在Unity ECS中使用)非常明确地构建为对与原型相关联的组件使用交错存储。

如果AI不影响用于实体渲染的变换或动画状态,则AI是毫无意义的。

仅仅因为AI无法有效地线性访问转换数据并不意味着没有其他系统可以有效地使用该数据布局优化。您可以使用打包数组来转换数据,而不必阻止游戏逻辑系统按常规方式执行游戏逻辑。

您还忘记了代码缓存。当您使用ECS的系统方法(与某些更幼稚的组件体系结构不同)时,您可以确保运行的是相同的小代码循环,并且不会在虚拟函数表中来回跳转到Update散落在各处的各种随机函数中您的二进制文件。因此,在AI案例中,您确实希望将所有不同的AI组件(因为肯定有多个组件,以便可以组成行为!)保存在单独的存储桶中,并分别处理每个列表,以便获得最佳的代码缓存使用率。

使用延迟的事件队列(系统会生成事件列表,但在系统完成对所有实体的处理之前不会调度事件),可以确保在保留事件的同时很好地使用了代码缓存。

通过使用一种方法,其中每个系统都知道要从该帧中读取哪些事件队列,您甚至可以使读取事件更快。至少比没有要快。

记住,性能不是绝对的。您无需消除所有最后的单个高速缓存遗漏,就可以开始看到优质的面向数据设计的性能优势。

仍在积极进行研究,以使许​​多游戏系统在ECS架构和面向数据的设计模式下更好地工作。与近年来我们用SIMD所做的一些令人惊奇的事情类似(例如JSON解析器),我们看到越来越多的ECS框架所做的事情对传统游戏体系结构而言似乎并不直观,但提供了许多好处(速度,多线程,可测试性等)。

也许有一种混合方法,每个人都在使用,但没人在谈论

这是我过去一直提倡的,尤其是对于那些对ECS体系结构持怀疑态度的人:对性能至关重要的组件使用面向数据的良好方法。使用更简单的体系结构,其中简单可以缩短开发时间。不要像ECS所建议的那样,将每个组件都塞进严格的组件化定义中。以这样的方式开发您的组件架构:您可以轻松地在有意义的地方使用类似于ECS的方法,而在没有ECS的意义(或者比树形结构更没有意义的东西)中使用更简单的组件结构。 。

我个人最近才转换为ECS的真正力量。尽管对我来说,决定因素是ECS很少提及的因素:与过去使用紧密耦合的基于逻辑的基于组件的设计相比,对游戏系统和逻辑的编写测试几乎微不足道。由于ECS体系结构将所有逻辑放入系统中,而这仅消耗组件并产生组件更新,因此构建一组“模拟”组件来测试系统行为非常容易;因为大多数游戏逻辑应该只存在于系统内部,所以有效地意味着测试所有系统将为您的游戏逻辑提供相当高的代码覆盖率。系统可以使用模拟依赖项(例如GPU接口)进行测试,其复杂性或性能影响比您少得多

顺便说一句,您可能会注意到,很多人在谈论ECS时并没有真正理解它的含义。我看到经典的Unity被称为ECS,它的使用频率令人沮丧,这说明太多的游戏开发人员将“ ECS”等同于“ Components”,并且几乎完全忽略了“ Entity System”部分。您会看到Internet上的ECS上充满了很多爱,而实际上大部分人只是在提倡基于组件的设计,而不是实际的ECS。在这一点上,争论起来几乎毫无意义。ECS已从其原始含义变为通用术语,您也可以接受“ ECS”与“面向数据的ECS”的含义不同。:/


1
如果要与基于组件的常规设计进行比较/比较,定义(或链接到)ECS的含义将很有用。我不清楚这个区别是什么。:)
内森·里德

非常感谢您的回答,看来我对此主题仍有很多研究要做。你有什么书可以指向我吗?
Haydn V. Harach

3
@NathanReed:ECS记录在诸如entity-systems.wikidot.com/es-terminology之类的地方基于组件的设计只是常规的基于继承的聚合,但是重点在于对游戏设计有用的动态组合。您可以编写不使用系统或实体(基于ECS术语)的基于组件的引擎,并且可以在游戏引擎中使用的组件远远超过仅游戏对象/实体,这就是我强调这一点的原因。
肖恩·米德迪奇

2
尽管网络上有很多文献,但这是我读过的有关ECS的最佳文章之一。超级赞。那么,肖恩,最终,您开发游戏的一般方法是什么(而不是复杂的方法)?纯ECS?基于组件和ECS的混合方法?我想知道更多关于您的设计!在Skype或其他方面进行讨论是否要求太多?
Grimshaw 2014年

2
@Grimshaw:gamedev.net是进行开放式讨论的一个不错的地方,我想也应该是reddit.com/r/gamedev(尽管我自己不是一个重做者)。我经常在gamedev.net上,与其他许多聪明人一样。我通常不进行一对一的对话;我非常忙,更喜欢将停机时间(即编译时间)用于帮助很多人而不是少数几个人。:)
肖恩·米德迪奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.