本文还在不断完善,可能不会及时同步在 SegmentFault,源文章在个人博客中:萤火之森 - Unity DOTS 蜻蜓点水html
简单介绍 Data-Oriented Technology Stack (DOTS, 数据导向型技术栈) ,其包含了 C# Job System、the Entity Component System (ECS) 和 Burst。git
DOTS 要实现的特色有:程序员
其中向量化指的是 Vectorization。github
向量化的相关介绍:编程
Unity 构建了名为 Burst 的代码生成器和编译器。数组
当使用 C# 时,咱们对整个流程有完整的控制,包括从源代码编译到机器代码生成,若是有咱们不想要的部分,咱们会找到并修复它。咱们会逐渐把 C++ 语言的性能敏感代码移植为 HPC# (高性能 C#,下文会提到)代码,这样会更容易获得想要的性能,更难出现 Bug,更容易进行处理。安全
若是 Asset Store 资源插件的开发者在资源中使用 HPC# 代码,资源插件在运行时代码会运行得更快。除此以外,高级用户也会经过使用 HPC# 编写出自定义高性能代码而受益。数据结构
ECS Track: Deep Dive into the Burst Compiler - Unite LA多线程
Burst 对于 HPC# 更详细的支持能够在下面找到:架构
向量化(Vectorization)没法进行的常见状况是,编译器没法确保二个指针不指向相同的内存,即混淆状况(Alias)。Alias 的问题在 Unity GDC 中也有一个演讲提到过:Unity at GDC - C# to Machine Code。
Collections 类就是为了解决这个问题而诞生的,里面包含 NativeList<T>、NativeHashMap<TKey, TValue>、NativeMultiHashMap<TKey, TValue> 和 NativeQueue<T> 四种额外的数据结构。
两个 NativeArray 之间从不会发生混淆这种状况,这也是为何咱们将会常用这些数据结构。咱们能够在 Burst 中运用这个知识,使它不会因为惧怕两个数组指针指向相同内存而放弃优化。
Unity 还编写了 Unity.Mathemetics 数学库,提供了不少像 Shader 代码的数据结构。Burst 也能和这数学库很好的工做,将来 Burst 将可以为 math.sin()
等计算做出牺牲精度的优化。
对于 Burst 而言,math.sin()
不只是要编译的 C# 方法,Burst 还能理解出 sin()
的三角函数属性,同时知道 x 值较小时会出现 sin(x)
等于 x 的状况,并了解它能替换为泰勒级数展开,以便牺牲特定精度。
跨平台和架构的浮点准确性是 Burst 将来的目标。
传统模式指的是什么呢?
离散的数据致使搜索效率十分低下,还有 Cache Miss 的问题,这个问题能够参考下面的连接:
例如当咱们要调用 Transform 时,可能实际上咱们只须要 position 和 rotation 两个属性来移动 gameObject,可是其余不须要的数据也被提供给了 gameObject。
传统模式只使用单线程来按顺序一个一个地处理数据和操做,这样十分低效。
当咱们使用 C# 语言时,仍然没法控制数据在内存中如何进行分布,但这是咱们提高性能的关键点。
除此以外,标准库面向的是“堆上的对象”和“具备其它对象指针引用的对象”。
也就是意味着,当处理性能敏感代码时,咱们能够放弃使用大部分标准库,例如:Linq、StringFormatter、List、Dictionary。禁止内存分配,即不使用类,只使用结构、映射、垃圾回收器和虚拟调用,并添加可以使用的部分新容器,例如:NativeArray 和其余集合类型。
咱们能够在越界访问时获得错误和错误信息,以及使用 C++ 代码时的调试器支持和编译速度。咱们一般把该子集称为高性能 C# 或 HPC#。
它能够被总结为:
NavtiveArray<T>
代替 T[]
throw new XXXException(...)
给予基础支持Job System 是针对上述传统模式问题的一种解决方式。例以下图能够把发射子弹当作一个 Job,从而用多线程来并行地处理发射操做。
目前主流的 CPU 有 4-6 个物理核心,8-12 个逻辑核心,多线程处理将可以更好地发挥 CPU 的性能。
传统的多线程问题也有不少:
而 Job System 就是专一解决上面问题的一个方案,这样咱们就能享受着多线程的好处来开发游戏。固然了,咱们也要写出正确的 ECS 代码,熟悉新的开发模式。
C++ 和 C# 都没法为开发者编写线程安全代码提供太多帮助。即便在今天,拥有多个核心游戏消费级硬件发展至今已通过去了十年,但依旧很难有效处理使用多个核心的程序。
数据冲突,不肯定性和死锁是使多线程代码难以编写的挑战。Unity 想要的特性是“确保代码调用的函数和全部内容不会在全局状态下读取或写入”。Unity 但愿应该让编译器抛出错误来提醒,而不是属于“程序员应遵照的准则”,Burst 则会提供编译器错误。
Unity 鼓励 Unity 用户编写 “Jobified” 代码:将「全部须要发生的数据转换」划分为 Job。
Job 会明确指定使用的只读缓冲区和读写缓冲区,尝试访问其它数据会获得编译器错误。Job 调度程序会确保在 Job 运行时,任何程序都不会写入只读缓冲区。Unity 也会确保在 Job 运行时,任何程序都不会读取读写缓冲区。
若是调度的 Job 违反了这些规则,咱们会获得运行时错误(一般这种错误会在竞态条件出现时获得)。错误信息会说明,你正在尝试调度的 Job 想要读取缓冲区 A,但你以前已经调度了会写入缓冲区 A 的 Job ,因此若是想要执行该操做,须要把以前的 Job 指定为依赖。
Unity 一直以组件的概念为中心,例如:咱们能够添加 Rigidbody 组件到游戏对象上,使对象可以向下掉落。咱们也能够添加 Light 组件到游戏对象上,使它能够发射光线。咱们添加 AudioEmitter 组件,可使游戏对象发出声音。
咱们实现组件系统的方法并无很好地演变。过去咱们使用面向对象的思惟编写组件系统,致使组件和游戏对象都是“大量使用 C++ 代码”的对象,建立或销毁它们须要使用互斥锁修改“id 到对象指针”的全局列表。
经过使用面向数据的思惟方式,咱们能够更好地处理这种状况。咱们能够保留用户眼中的优良特性,即只需添加组件就能够实现功能,而同时经过新组件系统取得出色的性能和并行效果。
这个全新的组件系统就是实体组件系统 ECS。简单来讲,现在咱们对游戏对象进行的操做可用于处理新系统的实体,组件仍称做组件。那么区别是什么?区别在于数据布局。
ECS 使用的数据布局会把这些状况看做一种很是常见的模式,并优化内存布局,使相似操做更加快捷。
ECS 会在内存中对带有相同组件(Component)集的全部实体(Entity)进行组合。ECS 把这类组件集称为原型(Archetype)。
下图的原型就是由 Position 组件、Velocity 组件、Rigidbody 组件和 Renderer 组件组成的。
若是一个实体只有三个组件(不一样于前面提到的原型),那么那三个组件就组成了一个新的原型。
下面的图来自 Unite LA 的一次演讲的讲义, 很遗憾那次演讲没有录制下来。讲义能够在这里找到。
ECS 以 16k 大小的块(Chunk)来分配内存,每一个块仅包含单个原型中全部实体的组件数据。
一个帖子中有人提供了更加形象的内存布局图,例如上半部分的原型由 Position 组件和 Rock 组件组成,其中整个原型占了一个块(Chunk),两个组件的数据分别存在两个数组中,里面还带着组件数据对应的实体的信息。
每一个原型都有一个 Chunks 块列表,用来保存原型的实体。咱们会循环全部块,并在每一个块中,对紧凑的内存进行线性循环处理,以读取或写入组件数据。该线性循环会对每一个实体运行相同的代码,同时为 Burst 创造向量化(Vectorization,能够参考 StackOverflow 的问题)处理的机会。
每一个块会被安排好内存中的位置,以便于快速从内存获得想要的数据,详情能够参考下面的文章。
Unity2018 ECS框架Entities源码解析(二)组件与Chunk的内存布局 - 大鹏的专栏 - CSDN博客
实体是什么?实体只是一个 32 位的整数 key (和一些额外的数据例如 index 和 version 实体版本,不过在这里不重要),因此除了实体的组件数据外,没必要为实体保存或分配太多内存。实体能够实现游戏对象的全部功能,甚至更多功能,由于实体很是轻量。
实体的性能消耗很低,因此咱们能够把实体用在不适合游戏对象的状况,例如:为粒子系统内的每一个单独粒子使用一个实体。
实体自己不是对象,也不是一个容器,它的做用是把其组件的数据关联到一块儿。
咱们没必要使用用户的 Update 方法搜索组件,而后在运行时对每一个实例进行操做,使用 ECS 时咱们只需静态地声明:我想对同时附带 Velocity 组件和 Rigidbody 组件的全部实体进行操做。为了找到全部实体,咱们只需找到全部符合特定“组件搜索查询”的原型便可,而这个过程就是由系统(System)来完成的。
不少状况下,这个过程会分红多个 Job ,使处理 ECS 组件的代码达到几乎 100% 的核心利用率。ECS 会完成全部工做,咱们只须要提供对每一个实体运行的代码便可。咱们也能够手动处理块迭代过程(IJobChunk)。
当咱们从实体添加或移除组件时,ECS会切换原型。咱们会把它从当前块移动到新原型的块,而后交换以前块的最后实体来“填补空缺”。
在 ECS 中,咱们还要静态声明要对组件数据进行什么处理,是 ReadOnly 只读仍是 ReadWrite 读写(Job System 一小节提到过的两种缓冲区)。经过肯定仅对 Position 组件进行读取,ECS 能够更高效地调度 Job ,其它须要读取 Position 组件的 Job 没必要进行等待。
大致上,实体提供纯粹的数据给系统,系统根据本身所须要的组件来得到相应的知足条件的实体,最后系统再经过多线程来基于 Job System 来处理数据。
这种数据布局也解决了 Unity 长期以来的困扰,即:加载时间和序列化的性能。如今从大型场景加载或流式处理 ECS 数据的时间,不会比从硬盘加载和使用原始字节多多少。
总的来讲,ECS 有如下好处:
对 ECS 的常见观点是:ECS 须要编写不少代码。所以,实现想要的功能须要处理不少样板代码。如今针对移除多数样板代码需求的大量改进即将推出,这些改进会使开发者更简单地表达本身的目的。
Unity 暂时没有实现太多这类改进,由于 Unity 如今正专一于处理基础性能。
太多样板代码对 ECS 游戏代码没有好处,咱们不能让编写 ECS 代码比编写 MonoBehaviour 更麻烦。
——Unity
而为网页游戏而生的基于 ECS 的 Project Tiny 已经实现了部分改进,例如:基于 lambda 函数的迭代 API。
因为本身空闲时间很少,只能囫囵吞枣地拼凑出这样一篇笔记。上面大部分文字都是来自 Unity 的博文介绍,本身加了其余的内容帮助理解。本文从内存布局介绍了 ECS 的概念,也介绍了 Job System 和 Burst。我相信走过一遍文章以后,能清楚 Unity 对数据驱动的将来开发趋势的布局,也能更加容易从 Unity ECS Sample 中理解如何实践 ECS。
Intro To The Entity Component System And C# Job System