对 CPU 缓存友好的实体组件系统框架

Posted

技术标签:

【中文标题】对 CPU 缓存友好的实体组件系统框架【英文标题】:Entity Component System Framework that is CPU cache friendly 【发布时间】:2014-06-21 19:58:21 【问题描述】:

我找不到一个对 CPU 缓存友好的框架实现,which means that data on which systems traverse in each game loop cycle is stored in a contiguous memory。

让我们看看,系统遍历满足其条件的特定实体,即该实体应包含要由 X 系统处理的 A、B、C 组件。这意味着我需要一个包含所有实体和组件的连续内存(不是引用,只要引用对缓存不友好,并且您将有很多缓存未命中),以便尽可能快地从 RAM 中获取它们可能在 X 系统的处理过程中。但是在处理完 X 系统之后,Y 系统开始对一组满足其条件 e 的实体进行操作。 g.所有包含 A 和 B 的实体。这意味着我们处理与 X 系统相同的一组实体以及一些具有 A 和 B 的其他实体。这意味着我们有两个具有重复数据的连续内存。首先,由于已知原因,数据重复非常糟糕。这反过来又意味着我们需要同步,这又不是 CPU 缓存友好的,因为您需要从一个向量中找到一些实体并使用另一个向量中包含的新数据进行更新。

这只是我的一个想法。对于实体组件系统框架数据模型还有其他更现实的想法,但在每个模型中我都发现存在相同的问题:在每个游戏循环周期中,由于数据不连续,您无法防止大量缓存未命中。

任何人都可以就这个主题提出一个实现、文章、示例的建议,这可以帮助我理解应该使用什么数据模型来获得缓存友好的设计,因为这是游戏性能中最关键的事情之一。

【问题讨论】:

【参考方案1】:

Adam Martin/t=machine 最近发布了Data Structures for Entity Systems: Contiguous memory - 这是我所知道的唯一一篇专门讨论 ECS 中的内存布局的文章。

您没有指定语言,但在 Java 世界中,entreri 和 artemis-odb(通过 PackedComponents / 也免责声明:我的端口)处理 Adam 所说的“迭代 1:每个 ComponentType 的 BigArray” .

【讨论】:

感谢您提供此链接。我对 ECS 很感兴趣,我想找到或了解如何编写一个缓存友好的 ECS。但问题是,当您想创建这样一个框架时,您将面临文章中描述的所有问题。而你唯一能做的就是做一些优化。但优化只是变通方法(请参阅 Adam Martin 在每次迭代后如何描述多个问题?),而不是解决方案。我什至不确定这个级别是否有解决方案。也许我们需要一种比经典 ECS 更好的方法,它既可以结合 ECS 的灵活性,又可以对缓存友好? 那篇文章只是对问题的“介绍”,对于那些不在 AAA 工作室工作、不自己做核心内存管理等的人。有很好的解决方案,但是他们需要仔细考虑您的目标硬件平台——大多数独立开发者都不会在意的知识水平。一些引用的方法适用于小型游戏;对于大型游戏,我不会使用任何(我有更好、更聪明的方法,我没有时间写下来。但这不是火箭科学——一个好的 C++ 开发人员可以为你指明正确的方向)跨度> 【参考方案2】:

我会选择 junkdog 的回答(因为我写了链接的文章 ;)),但这是另一个不同的,接受它:

如果你想要缓存友好的设计,你需要列出:

    您的微处理器 您的处理器架构 您的总线架构 ... 您的每个子帧工作集大小 所有游戏对象的总工作集/RAM 特定游戏中的互连数量 ...等

根据这些要求的严格程度/松散程度,您将对设计做出不同的简单(或困难)决定。游戏开发者经常重写内存管理。他们这样做不是因为他们很愚蠢,而是因为对每个项目进行(重新)优化很容易/值得(这是 AAA 标题?还是 AA 标题?图形更重要吗?还是网络延迟?。 .. 等)和每个硬件(在 PC 上,目标硬件每个月都会更改)

我建议你选择一套硬件,创建一个简单的基于 ES 的游戏,然后开始尝试设计一个缓存友好的内存用法 - 并公开记录它,使其全部开源,看看你是否可以获得其他有兴趣运行您的基准测试的人。

【讨论】:

谢谢你的回答。我喜欢互联网,它可以让如此伟大的人交流。就我而言,游戏是适用于 androidios 的手机游戏。所以我根本没有任何特定的硬件。但是我看到没有用 C++ 实现的 ECS 来解决缓存友好性问题。我使用 Artemis 移植到 C++,它还不错,但我对它的设计/实现印象不深。其他语言有更好设计的 ECS 框架,但其他语言比 C++ 具有非常弱的缓存友好能力,因为它们不直接处理内存(C#、Java 等)。 Java 直接使用内存,您的信息已经过时了 10 年(!) - 请阅读有关 ByteBuffer 的信息。我相信 C# 的内存管理也比您建议的要好(尽管我不记得它是否可以在没有 shim 的情况下直接访问内存?) 当我使用new PositionComponent(0, 0) 创建组件并且它是在堆中创建时,ByteBuffer 如何提供帮助? 阅读文档。去谷歌上查询。那里有一个完整的 API。调查一下。如果您不知道如何使用它,请提出新问题。【参考方案3】:

理论上我认为这个问题需要付出太多努力才能证明完美解决它所花费的时间是合理的。过去我自己已经在这方面花费了太多时间,想出复杂的解决方案只是为了回到一个更简单的解决方案。我们最大的热点不一定来自实体/组件遍历的非强制缓存未命中。许多系统将为可以加速的特定实体承担大量工作,并且许多组件通常会大到足以减少尝试以适合多个邻居的方式对它们进行排序的好处。缓存行。

也就是说,如果您只是想对组件进行排序,以提供对缓存友好的内存访问模式,但仅适用于一两个关键系统而不会发生重叠冲突,并且可能适用于最小和最多的组件类型这肯定会帮助最大,这很容易在这里和那里进行一些后期处理。我建议您寻找它来响应您的热点。

而且通常只是一些基本的排序将帮助您减少所有系统的缓存未命中份额,而不管它们处理的组件组合是什么。如果您从这样的代表开始(我使用的):

在运行游戏状态并偶尔删除和添加组件一段时间后,您最终会得到如下结果:

你可以像这样解开混乱并整理出来:

这可以通过基数排序非常便宜地完成,基于拥有它们的实体索引作为线性时间的键对元素进行排序。通过一个体面的实现,您通常可以偷偷摸摸,而不会注意到帧速率有任何问题。我以与上面的数据表不同的方式绘制图表(只是为了清楚哪个组件属于哪个实体),但想法相同。只需根据实体索引(实体 ID)对组件数组进行基数排序,更新链接(使用并行数组映射之前/之后的索引,使用实体索引作为键与组件数据一起排序),现在一切一切都很好,整洁,没有与零星的访问模式纠缠在一起。

这可能不会为对特定实体组合感兴趣的系统提供一组完全连续的组件(可能存在上图中的一些间隙),但至少它不会来回反复在内存中,可能不得不将内存区域加载到缓存行中只是为了驱逐它,然后返回并再次重新加载它,并且在这些情况下很可能会连续访问许多组件。

如果这还不够好,那么给定所讨论的特定实体恰好具有系统对特定查询感兴趣的组件,您可以根据系统想要的那些特定实体将组件排序到数组的顶部,缩小任何差距,现在您对系统具有完美的连续性,专门处理包含运动和渲染组件的实体。这也可以在线性时间内完成,并在这里和那里进行后期处理,可能会在删除和添加许多组件后定期应用。

我从未发现有必要走这么远。我只是不时对实体 ID 进行广义排序,以普遍改进所有系统的访问模式(但没有针对任何给定系统的最佳解决方案)。您的用例可能需要一个最佳版本,但我建议只关注具有大热点且真正受益的关键系统。

【讨论】:

以上是关于对 CPU 缓存友好的实体组件系统框架的主要内容,如果未能解决你的问题,请参考以下文章

Microsoft 对实体框架中的二级缓存有何建议?

部署Django

Django的介绍

基于.NET平台常用的框架整理

是否可以使用实体框架将 SQL Server 的 rowversion 类型映射到比 byte[] 更友好的东西?

实体框架缓存的查询计划性能随参数不同而降低