DOTS-研究学习
1. What is DOTS?
DATA-ORIENTED TECH STACK
多线程式数据导向型技术堆栈
核心-高性能
充分利用多核处理器,多线程,让游戏运行速度更快。
组成部分如下:
- C# 任务系统, Job System,用于高效运行多线程代码
- 实体组件系统,ECS, 默认编写的高性能代码的框架结构
- Burst编译器,用于生成高度优化的本地代码
Job System 与ECS是独立的,两者结合实际才能发挥最大优势。
2. Job System
- 在Job System之前,unity 仅在内部支持多线程,外部都必须运行在主线程上
- C#虽然支持多线程,但在unity中只处理数据,如网络消息,下载等等,且在线程中调用Unity的API是不行的。
- 有了Job System,可以充分利用多核CPU,比如在多线程中修改Transform等
- 例:MMO游戏判断碰撞,大量同步角色坐标,大量血条飘字等较为适合
- 不必过于担心线程安全,加锁的问题
- 配合Burst更佳
2.1 HPC# -High Performance C#
介绍:
- IL2Cpp虽然将IL转成C++,但实际还是模拟了.NET的垃圾回收,效率并非等同C++
- 使用NativeArray代替T[],数据类型包括了值类型和其他类型指针
- NativeArray可以在C#层分配 C++中的对象,主动释放而不进行C#的垃圾回收。
Burst性能对比:
- .Net Code 比C++慢2倍
- Mono比.NET Code慢3倍
- IL2CPP与.NET Code相当
- Burst将比C++运行更快。
http://aras-p.info/blog/2018/03/28/Daily-Pathtracer-Part-3-CSharp-Unity-Burst/
2.2 代码使用
- 数据类型只能是值类型和其他类型的指针
- 不能使用引用类型,如T[]数组就不能在job中使用,应用HPC#的NativeArray代替
IJob与IJobParallelFor
IJob是一个一个的开线程任务,顺序执行的,所以正确性有保证
如果想让线程真正并行,则应使用JobParallelFor。这样并行后的数据就不会有前后依赖关系。
使用ReadOnly标记的数据,只读后让Job不为其加锁。
默认的数据是Read/Write的,这样在改变数据后,Job一定会等它。
加解锁unity已经做好了,不需要自己实现逻辑。
3.Burst编译器
3.1 原理
- 以开源LLVM为基础的后端编译技术
- 原理的5个步骤:源代码》前端》优化器》后端 》机器码
- LLVM定义抽象语言IR,前端负责将源代码(C#)翻译成IR,优化器优化,后端将IR生成目标语言(机器码)。
- IR的存在,所以LLVM支持众多语言
- LLVM对C#的GC支持不好,目前burst只支持值类型。
3.2 内存别名
- 之前的编译器无法知道运行时两个指针指向同一个地址的情况,编译出的代码必然要占用额外寄存器多次拷贝,无法优化。
- NativeArray的API禁止限制了内存别名,不存在两个指针指向同一地址的情况。所以它更加高效。
3.3 Unity.Mathmatcis数学库
- 更高效的数据库,提供矢量类型float3,float4. 直接映射到SIMD寄存器
- Math类中也提供了直接映射 到硬件SIMD寄存器。
- SIMD可以一次性计算完毕。
- 旧Math类是不支持SIMD寄存器的。
3.4 启动BurstCompile
- 添加标签即可:
- Struct必须实现IJob接口
4. ECS
4.1 组合模式:
需要什么功能加什么组件
4.2 传统脚本的问题:
- 耦合度过高
- 引用类型
- 挂脚本过重,产生 0.01s耗时
- 热更支持不好
4.3 Entity 实体
A entity is a key.
- 非常轻量级,一个ID用Int保存。
- 根据需要绑定组件
- 对Cache友好,将相同的组件排列在一起,遍历更快。
4.4 Component 组件
数据层,只有数据
- 实现IcomponentData接口的结构体,不能写方法,没有任何行为
- 注意使用新的数学库代替一些变量。
ArcheType原型
组件命中率高得益于Archetype
unity规定每个archetype为16K,不够再开,保证连续性
共享组件
区别IcomponentData于,IsharedComponentData共享组件。是为了避免结构体存储的数据完全相同,从而占用多份内存
典型的例子:场景中很多相同mesh和材质的物体。
必须实现IEquatable接口!
4.5 System系统
A system is a data tranform.
- 只与Component关联,不关心entity
- 在Update中可以统一更新自己关心的组件
4.6 World世界
Worlds ars for isolation
- 包含EntityManager, ComponentSystem, ArcheTypes.
- ECS默认提供了一个World,也可自己创建
- 世界之间不互通,可以同时并行
5. ECS + Job + Burst让性能飞起来
5.1 JobComponentSystem
- 使用继承于ComponentSystem的JobComponentSystem
- JobComponentSystem的Update效率更高,复杂运算将由Job完成
- 标识ReadOnly让Job的数据并行
- 如果Job数据变更,此Job就不能与其他访问此数据的JOB并行
5.2 JCS与CS混合
- JCS中的实体有任何 结构数据变更修改都带来硬性同步点
- JCS必须保证后面执行的CS拿到的数据准确性,实体增删改都会带来同步点,导致线程卡顿。
5.3 ECS渲染
- ECS自身不包含渲染,但游戏中的渲染与实体是紧密绑定的
- 原理大致是ECS在Job中先准备渲染的数据,通过GPU Instancing一次渲染,中间不产生gameobject.
- GPU instancing不带裁剪,且需要每帧在Update中调用刷新。建议使用CommandBuffer来渲染Gpu instancing,如果在有变化时再刷新。
https://www.xuanyusong.com/archives/4683
- 使用BatchRendererGroup代替Graphics.DrawMeshInstanced和CommandBuffer.DrawMeshInstanced
- BRG强制需要镜头裁剪的JOB方法,自己实现
- BRG需要提供每个渲染物体的包围盒区载用于job中判断是否不在视野。
- BRG内部会调自动Graphics.DrawMeshInstanced且没有1023的数量限制。
6.实践
6.1 装Entites, Burst, Hybrid Render, Dots Editor
报错了?The type or namespace name ‘CompilerServices’ does not exist in the namespace ‘Unity.Burst’ (are you missing an assembly reference?)
选择安装preview下的最高版本,如burst会默认安装非preview的版本
6.2 实例1 创建entity
using UnityEngine; |
注意: RenderBounds 如果不加,会导致不显示。。
6.2 实例2 自定义system
System定义后,将自动执行update函数。
using Unity.Entities; |
6.3 实例3 加入JOB与Burst
在实例2的基础上加上Job和Burst
|
7. 使用Conversion
流程明细:
// ??? <- IDeclareReferencedPrefabs 调用. |
7.1 ConversionToEntity
- 为GameObject添加ConversionToEntity将自动转换为ECS的entity.
- 添加ConversionToEntity(Stop)将保持为原gameobject
7.2 ConversionMode
- Convert And Destroy模式:如果没有conversion system处理的组件,在Conversion world 被带入的东西在world销毁时也同时销毁。
- Convert and inject 模式: 此模式下,相比上一个组件都会保留。
Convert and inject模式是用于当你需要在 conversion 的结果上面回溯原有对象的情况下才有用, 而 Hybrid Renderer 显然无需回溯。
7.3 IConvertGameObjectToEntity
- 继承此接口,在方法Convert里自由控制conversion的过程
Unity 内置了另一种名为ConvertGameObjectToEntitySystem 的 conversion system. 该 system 会迭代 conversion world 中所有的GameObject, 接着使用GetComponents 并判断是否实现IConvertGameObjectToEntity 接口, 然后再调用该接口的.Convert 方法
7.4 LinkedEntityGroup
- 为原层次结构做关联的组件。同时也是一个dynmaic buffer。
- 调用Instantiate方法时, 会同时实例化所有 buffer 中的 entity, 同时也会创建相同的LinkedEntityGroup. 注意实例化并不一定和ECS中的Prefab component 直接关联.
- 调用DestroyEntity时也会同时销毁 LinkedEntityGroup中的所有 entity. 类似在编辑器中删除GameObject
- 调用 entityManager.SetEnabled 加上的 Disabledcomponent 会告知 ECS 的查询系统忽略它们, 而 LinkedEntityGroup 中的 entity 也会受到同样的影响. 有点类似禁用GameObject 时同时会禁用整个层级树.
当有子物体Disable状态时,此子物体自身不会生成linkedentitygroup的子结构,但可以手动在此disable上添加
注意如果buffer 中的 entity 也有LinkedEntityGroup, 系统不会递归地执行instantiation/destroy/disabled 过程.
这些过程在具体执行当中也有一些细微不同.Instantiate和SetEnabled只要检测到 buffer 便在所有成员上一次性执行, 不会做其他更多事. 这意味着关联该 buffer 的 entity 必须要把自己包括在内才能正常工作. 然而DestroyEntity则无所谓, 因为它会先销毁传入的entity, 然后再迭代 buffer 中的 entity 进行销毁.
要注意LinkedEntityGroup 和 Parent并不一样 (虽然它们经常同时出现). 后者是递归地工作, 循环依赖也是不允许的.
- 为gameobject添加
public class CubeConvert : MonoBehaviour, IConvertGameObjectToEntity |
在 entities-0.5.1版本里, TypeManager.cs 源码中可看到, 任何没有[InternalBufferCapacity] 的buffer 类型都会默认 128/size 的容量. LinkedEntityGroup里面装的是 entity, 因此其容量是 128/8 = 16 .
每个 Entity 关联的LinkedEntityGroup(或者其他未指定 capacity 的buffer) 将占据 128 bytes. 这是 chunk 容积变小的原因.
层级中超过16个子对象并不是什么好事, 一旦超过这个数量, 这些 linked entities 不得不从排列良好的 chunk 内存中挪到堆内存中. 可能 Unity 认为 16 是一个不太可能达到的值, 而 8 又太过于常见.
除非显式调用, LinkedEntityGroup仅仅在 prefab 的 conversion 过程中被自动创建, 因此你只需要注意你的 prefab 里面嵌套的 GameObject 数量
在运行时, 所有嵌套 prefab 和 prefab variant 工作流并不受影响, 系统内部只把它们看过一个单独的 prefab. 你无法把嵌套 prefab 从父 prefab 中拿出来, 并期望LinkedEntityGroup正常工作.
最后, 16个entities 一个 chunk , 1MB 大概包含 60 个 chunk. 如上例, 你能在 1MB 存储大约 2700 个转换后的GameObject , 这样看来或许45 的 chunk 容量也不用太担心了(当然具体情况具体分析).
7.5 创建额外的entity
使用如下方式:
Entity additional1 = conversionSystem.CreateAdditionalEntity(this.gameObject); |
注意:此方式创建的entity将不在linkedentitygroup内,且是完全空的entity