如何从实体组件系统游戏引擎中的cpu缓存中受益?


15

我经常阅读ECS游戏引擎文档,这是明智使用cpu缓存的良好架构。

但是我不知道如何从cpu缓存中受益。

如果将组件保存在连续内存中的数组(或池)中,则只有在顺序读取组件的情况下,才是使用cpu缓存BUT的好方法。

当我们使用系统时,它们需要实体列表,这些列表是具有特定类型组件的实体列表。

但是这些列表以随机的方式而不是顺序地提供了组件。

那么如何设计ECS以最大化缓存命中率呢?

编辑:

例如,物理系统需要具有RigidBody和Transform组件的实体的实体列表(有一个RigidBody池和一个Transform组件池)。

因此,其更新实体的循环将如下所示:

for (Entity eid in entitiesList) {
    // Get rigid body component
    RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);

    // Get transform component
    Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);

    // Do something with rigid body and transform component
}

问题在于,entity1的RigidBody组件可以在其池的索引2处,而entity1的Tranform组件在其池的索引0处(因为某些实体可以具有某些组件,而其他组件不能具有其他组件,并且是因为添加/删除了实体/组件)。

因此,即使组件在内存中是连续的,也会随机读取它们,因此它将有更多的缓存未命中,不是吗?

除非有一种方法可以预取循环中的下一个组件?


您能告诉我们如何分配每个组件吗?
concept3d 2013年

通过一个简单的池分配器和一个句柄管理器,可以使用组件引用来管理池中组件的重定位(以使组件在内存中保持连续)。
约翰内弗(Johnmph)2013年

您的示例循环假定组件更新是按实体交错的。在许多情况下,可以按组件类型批量更新组件(例如,首先更新所有刚体组件,然后使用完成的刚体数据更新所有变换,然后使用新的变换更新所有渲染数据...)-这样可以改善缓存用于每次组件更新。我认为尼克·威吉尔(Nick Wiggill)在下面提出了这种结构。
DMGregory

这是我的示例,但很糟糕,实际上,它是“用完成的刚体数据更新所有变换”系统,而不是“物理”系统。但是问题仍然存在,在这些系统中(使用刚体更新变换,使用变换更新渲染...),我们将需要同时具有一种以上类型的组件。
约翰汉普(Johnmph)2013年

不确定这是否也相关?gamasutra.com/view/feature/6345/...
DMGregory

Answers:


13

Mick West的文章全面介绍了线性化实体组件数据的过程。几年前,它在Tony Hawk系列产品上使用了比现在少得多的令人印象深刻的硬件,从而极大地提高了性能。他基本上对每种不同类型的实体数据(位置,得分和诸如此类)使用了全局的,预先分配的数组,并在他的系统范围update()功能的不同阶段引用了每个数组。您可以假设每个实体的数据在这些全局数组的每个数组中都位于相同的数组索引,因此,例如,如果首先创建播放器,则它的数据可能[0]在每个数组中。

Christer Ericsson针对C和C ++ 的幻灯片甚至更具体地介绍了缓存优化。

为了提供更多细节,您应该尝试针对每种数据类型(例如position,xy和z)使用连续的内存块(最容易分配为数组),以确保良好的引用局部性,并在每个数据块中分别使用update()为了临时性起见,请在各个阶段进行操作,即确保在给定update()调用中重用要重用的任何数据之前,不通过硬件的LRU算法刷新高速缓存。正如您所暗示的那样,您不想通过分配实体和组件为离散对象new,因为每个实体实例上不同类型的数据将被交织,从而减少了引用的位置。

如果您在组件(数据)之间存在相互依赖关系,以至于您绝对无法承受将某些数据与其关联数据(例如,Transform + Physics,T​​ransform + Renderer)分开的话,则可以选择在Physics和Renderer阵列中复制Transform数据。 ,以确保所有相关数据都适合每个关键性能操作的缓存行宽度。

还请记住,L2和L3缓存(如果可以在目标平台上假设这些缓存)在减轻L1缓存可能遭受的问题(例如行宽限制)方面起到了很大的作用。因此,即使在一次L1丢失时,这些安全网也通常会阻止对主内存的调用,这比对任何级别的缓存的调用要慢几个数量级。

关于写入数据的注意事项写入不会调出到主存储器。默认情况下,当今的系统启用了写回缓存:写一个值仅将其写到(初始)缓存中,而不是写到主内存中,因此您不会因此而成为瓶颈。仅当从主内存请求数据时(缓存中不会发生数据)并且陈旧时,才会从缓存中更新主内存。


1
对于任何不std::vector熟悉C ++的人,请注意:基本上是一个可动态调整大小的数组,因此也是连续的(在较早的C ++版本中是事实,而在较新的C ++版本中实际上是)。的某些实现std::deque也“足够连续”(尽管不是Microsoft的)。
肖恩·米德迪奇

2
@Johnmph很简单:如果您没有参考地区,那么您将一无所有。如果两个数据紧密相关(例如空间和物理信息),即它们被一起处理,那么您很可能不得不将它们压缩为一个单独的成分,并交织在一起。但是请记住,由于没有将空间数据包括在其中,因此利用该空间数据的任何其他逻辑(例如AI)都可能遭受损失。因此,这取决于最需要什么性能(在您的情况下可能是物理性能)。那有意义吗?
工程师

1
@Johnmph是的,我完全同意Nick的看法,这是关于它们如何存储在内存中,如果您的实体带有指向两个组件的指针,而这两个组件在内存中的位置很远,那么您就没有本地性,那么它们就必须放在高速缓存行中。
concept3d 2013年

2
@Johnmph:确实,Mick West的文章假设相互依存性最小。因此:最小化依赖关系;沿缓存行复制数据,在这些行中您无法最小化这些依赖性...例如, RigidBody Render 旁边包含Transform ;并且为了适应高速缓存行,您可能需要尽可能减少数据原子……这可以通过从每个小数点值的浮点到固定点(4个字节对2个字节)进行部分实现。但无论如何,无论如何,您的数据都必须符合concept3d所述的高速缓存行宽度,以实现最佳性能。
工程师

2
@Johnmph。否。无论何时写入Transform数据,只需将其写入两个数组中。不是那些您需要担心的文章。一旦发送了写操作,它就完成了。当您运行Physics and Renderer时,是在更新中稍后的读取操作必须立即在单个高速缓存行中访问所有相关数据,并且该高速缓存对CPU来说是私有的。另外,如果您真的需要全部,那么您可以做进一步的复制,或者确保物理,变换和渲染适合单个缓存行... 64字节是常见的,实际上是很多数据!...
工程师
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.