ECS 为什么会高效,性能好,Entity的内存布局与分配就是非常重要的部分,今天我们一起来分析一下Unity ECS 架构里面如何来做高效的内存分配器。这种思路也可以给我们做内存分配提供很好的一个思路。
对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习unity的零基础小白,也有一些正在从事unity开发的技术大佬,欢迎你来交流学习。
1: ECS 里面基本的一些概念
Unity ECS框架里面有几个重要的概念:
Entity, ComponentData, System,Archetype, EntityManager, World;
ComponentData:
组件数据,开发的时候,可以把每个功能相关的数据放到ComponentData里面。
Entity:
对象实体,纯数据对象,里面包含了一个个的ComponentData, 每个相关功能使用的数据我们放Entity的ComponentData里面, 如图:
(Entity内存对象布局示意图)
System:
具体算法逻辑的实现,System算法数据基于Entity里面对应处理的ComponentData。游戏引擎每次Update的时候,System都会根据每个Entity里面的它使用的ComponentData, 来更新数据完成计算。
EntityManager:
负责Entity相关类型信息,对象实体的创建与管理。
Archetype:
Entity是由多个ComponentData组成的,每种Entity类型都会对应一个Archetype, 里面描述了这个Entity类型以及相关的布局。Archetype由EntityManager创建出来,创建出来后这个类型的Entity的大小,包含的组件数据,内存布局都确定了。
World:
世界包含多个Entity组成Entity群体,通过世界把这些Entity群体孤立起来。Unity里面可以同时拥有多个不同的世界, 每个世界独立包含EntityManager, 与Systems, 在世界里面创建出来的一个Entity,只属于这个世界。世界里每个System也只能迭代一个世界里面的Entity实体数据。你可以创建多个World。
总结一下Unity ECS的使用步骤与逻辑关系:
(1) 创建一个ECS的World;
(2) 使用World的EntityManager为不同Entity类型创建出来Archetype;
(3) 基于Archetype, 我们创建出来Entity内存对象, 并初始化Entity里面每个ComponentData数据;
(4)为ECS World编写System算法,World会调用System算法来迭代每个Entity里的相关ComponentData;
(Unity ECS 架构图)
2: ECS 高效的内存模型与布局
作为架构师,设计框架数据结构由为关键。特别对于游戏开发,要处理迭代成百上千个游戏物体。高效的内存模型与布局如何实现? 首先要明白怎么样布局内存才会高效。内存模型布局高效主要从两个方面来考虑:
1:内存对齐。
CPU访问不同地址的内存数据的时候,如果内存地址基于2^n 数据对齐(n根据CPU而定),访问最为高效。例如我们以16字节数据对齐,那么对象内存开始排布的时候, 从地址能整除16的内存位置开始排布。高效的内存布局,对象实例内存地址要对齐,对象实例里面的每个数据成员内存地址也要对齐。Entity的内存分配与内存布局, 在创建Entity的时候,ECS系统就会注意Entity的内存对齐,同时也会注ComponentData在Entity里面排布的内存对齐,如果没有对齐,就会空一些出来。比如16字节对齐,一个ComponentData只有14个字节,实际上排布下一个ComponentData的时候是从16字节开始排布的。
2: CPU通过地址来取到内存数据的时,数据排布一起取的速度会更快。
ECS天然就具有这种优势。所有的逻辑代码迭代都是基ComponentData的,也就是算法迭代访问处理的数据都在一起,在一个ComponentData里面。这样访问数据高效,避免了地址的来回跳转。
3: Entity内存分配器 Chunk的设计
大量的Entity的创建与销毁,大量不同类型的数据对象交叉创建,很容易造成内存碎片。何为内存碎片?给大家举个例子,对象A,对象B(100字节),对象C连续排布,对象B销毁,内存释放,同时又请求分配对象D(80字节),系统在原来对象B的地方把一块内存分配给对象D,还剩了一小块内存(20字节)。而这一小块内存,无法分配给当前系统的任何一个对象,这样,这小快内存就再也无法使用了,造成内存碎片,随着系统不断运行,随着更多大量的创建与释放,内存碎片越来越多,分配/释放效率也越来越低。最终让系统越来越慢。解决这种大量创建与销毁,我们一般采用Cache内存池来做。那Unity ECS架构里面内存池是如何设计的呢?接下来我们来分析ECS里面基于Chunk的内存分配器。不同的Archetype,所对应的Entity的内存不一样,系统可能有N中不同的Archetype,而且Archetype是用户根据组件数据组合创建而来。
综上Unity ECS 是这样设计内存分配缓存池:
(1) 基于固定大小的Chunk来做内存缓存池。每个Chunk大小为16KB(引用来自于UnityECS 文档)
(2) 当Archetype 确定后,Entity的内存大小就确定下来,每个Chunk能分配的当前的Entity数目就能确定下来。基于Archetype创建Entity的时候,首先从内存池里面拿一个Chunk, 然后根据每个Chunk可创建Entity数目,从Chunk里面把Entity分配出去,如果chunk分配完毕,从chunk内存缓存池里面再拿一个Chunk。由于基于Archetype创建,这个Archetype的所有Entity内存大小都是一样的,等释放的时候,我们就可以基于Archetype把Entity缓存起来,重复使用之前的Entity。
对于OS而言,ECS框架基于chunk来分配和释放内存,避免了内存碎片(大小都一样)。对于ArcheType而言,又基于特定ArcheType的Entity来做内存缓存,分配和释放都非常高效。
看完Unity ECS的架构设计与内存布局,我们在其他地方做ECS架构设计(游戏服务器)就有了一个很好的参考,设计万变不离其中。