https://segmentfault.com/a/1190000019143037

本文还在不断完善,可能不会及时同步在 SegmentFault,源文章在我的博客中:萤火之森 - Unity DOTS 走马观花

简单介绍 Data-Oriented Technology Stack (DOTS, 数据导向型技术栈) ,其包含了 C# Job System、the Entity Component System (ECS) 和 Burst。

特点

DOTS 要实现的特点有:

  • 性能的准确性。我们希望的效果是:如果循环因为某些原因无法向量化,它应该会出现编译器错误,而不是使代码运行速度慢8倍,并得到正确结果,完全不报错。
  • 跨平台架构特性。我们编写的输入代码无论是面向 iOS 系统还是 Xbox,都应该是相同的。
  • 我们应该有不错的迭代循环。在修改代码时,可以轻松查看为所有架构生成的机器代码。机器代码“查看器”应该很好地说明或解释所有机器指令的行为。
  • 安全性。大多数游戏开发者不把安全性放在很高的优先级,但我们认为,解决 Unity 出现内存损坏问题是关键特性之一。在运行代码时应该有一个特别模式,如果读取或写入到内存界限外或取消引用 Null 时,它能够提供我们明确的错误信息。

其中向量化指的是 Vectorization。

向量化的相关介绍:

Burst

Unity 构建了名为 Burst 的代码生成器和编译器。

当使用 C# 时,我们对整个流程有完整的控制,包括从源代码编译到机器代码生成,如果有我们不想要的部分,我们会找到并修复它。我们会逐渐把 C++ 语言的性能敏感代码移植为 HPC# (高性能 C#,下文会提到)代码,这样会更容易得到想要的性能,更难出现 Bug,更容易进行处理。

如果 Asset Store 资源插件的开发者在资源中使用 HPC# 代码,资源插件在运行时代码会运行得更快。除此之外,高级用户也会通过使用 HPC# 编写出自定义高性能代码而受益。

ECS Track: Deep Dive into the Burst Compiler - Unite LA

Burst 对于 HPC# 更详细的支持可以在下面找到:

Burst User Guide

深入栈

向量化(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 未来的目标。

传统模式的问题

传统模式指的是什么呢?

  • 跟 MonoBehaviours 打交道
  • 数据和其处理过程耦合在一起
  • 高度依赖引用类型

问题一:数据分布在内存的各个角落

离散的数据导致搜索效率十分低下,还有 Cache Miss 的问题,这个问题可以参考下面的链接:

ECS的泛泛之谈

问题二:很多不必要的数据也被提供了

例如当我们要调用 Transform 时,可能实际上我们只需要 position 和 rotation 两个属性来移动 gameObject,但是其他不需要的数据也被提供给了 gameObject。

问题三:低效的单线程数据处理

传统模式只使用单线程来按顺序一个一个地处理数据和操作,这样十分低效。

高性能 C#(HPC#)

当我们使用 C# 语言时,仍然无法控制数据在内存中如何进行分布,但这是我们提升性能的关键点。

除此之外,标准库面向的是“堆上的对象”和“具有其它对象指针引用的对象”。

也就是意味着,当处理性能敏感代码时,我们可以放弃使用大部分标准库,例如:Linq、StringFormatter、List、Dictionary。禁止内存分配,即不使用类,只使用结构、映射、垃圾回收器和虚拟调用,并添加可使用的部分新容器,例如:NativeArray 和其他集合类型。

我们可以在越界访问时得到错误和错误信息,以及使用 C++ 代码时的调试器支持和编译速度。我们通常把该子集称为高性能 C# 或 HPC#。

它可以被总结为:

  • 大部分的原始类型(float、int、uint、short、bool...),enums,structs 和其他类型的指针
  • 集合:用 NavtiveArray<T> 代替 T[]
  • 所有的控制流语句(除了 try、finally、foreach、using)
  • 对 throw new XXXException(...) 给予基础支持

Job System

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 指定为依赖。

Entity Component System

Unity 一直以组件的概念为中心,例如:我们可以添加 Rigidbody 组件到游戏对象上,使对象能够向下掉落。我们也可以添加 Light 组件到游戏对象上,使它可以发射光线。我们添加 AudioEmitter 组件,可以使游戏对象发出声音。

我们实现组件系统的方法并没有很好地演变。过去我们使用面向对象的思维编写组件系统,导致组件和游戏对象都是“大量使用 C++ 代码”的对象,创建或销毁它们需要使用互斥锁修改“id 到对象指针”的全局列表。

通过使用面向数据的思维方式,我们可以更好地处理这种情况。我们可以保留用户眼中的优良特性,即只需添加组件就可以实现功能,而同时通过新组件系统取得出色的性能和并行效果。

这个全新的组件系统就是实体组件系统 ECS。简单来说,如今我们对游戏对象进行的操作可用于处理新系统的实体,组件仍称作组件。那么区别是什么?区别在于数据布局。

ECS 数据布局

ECS 使用的数据布局会把这些情况看作一种非常常见的模式,并优化内存布局,使类似操作更加快捷。

原型(Archetype)

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博客

实体(Entity)

实体是什么?实体只是一个 32 位的整数 key (和一些额外的数据例如 index 和 version 实体版本,不过在这里不重要),所以除了实体的组件数据外,不必为实体保存或分配太多内存。实体可以实现游戏对象的所有功能,甚至更多功能,因为实体非常轻量。

实体的性能消耗很低,所以我们可以把实体用在不适合游戏对象的情况,例如:为粒子系统内的每个单独粒子使用一个实体。

实体本身不是对象,也不是一个容器,它的作用是把其组件的数据关联到一起。

系统(System)

我们不必使用用户的 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 有以下好处:

  • 为性能而生
  • 更容易写出高度优化和可重用的代码
  • 更能充分利用硬件的性能
  • 原型的数据被紧密地排列在内存中
  • 享受 Burst 编译器带来的魔法

缺点

对 ECS 的常见观点是:ECS 需要编写很多代码。因此,实现想要的功能需要处理很多样板代码。现在针对移除多数样板代码需求的大量改进即将推出,这些改进会使开发者更简单地表达自己的目的。

Unity 暂时没有实现太多这类改进,因为 Unity 现在正专注于处理基础性能。

太多样板代码对 ECS 游戏代码没有好处,我们不能让编写 ECS 代码比编写 MonoBehaviour 更麻烦。
——Unity

而为网页游戏而生的基于 ECS 的 Project Tiny 已经实现了部分改进,例如:基于 lambda 函数的迭代 API。

最后

由于自己空闲时间不多,只能囫囵吞枣地拼凑出这样一篇笔记。上面大部分文字都是来自 Unity 的博文介绍,自己加了其他的内容帮助理解。本文从内存布局介绍了 ECS 的概念,也介绍了 Job System 和 Burst。我相信走过一遍文章之后,能清楚 Unity 对数据驱动的未来开发趋势的布局,也能更加容易从 Unity ECS Sample 中理解如何实践 ECS。

参考

Unity DOTS 走马观花的更多相关文章

  1. 视频发布 2019 中国.NET 开发者峰会

    2019 年,注定会是 .NET Core 社区发展的关键一年,诸多重大事件在这一年发生!正如大家所期待的那样,刷新中国 .NET 社区的年度盛会--2019 中国 .NET 开发者峰会(.NET C ...

  2. Unity-ECS(一)浅谈CPU缓存命中和Unity面向数据技术栈(DOTS)--笔记

    一,缓存类型 概念:局部性. 时间局部性:当前用到的一个存储器位置,不久的将来会被用到. 空间局部性:当前用到的一个存储器位置,附近的位置会被用到. 那么在CPU的层面,这两个局部性的特性就会被Cac ...

  3. Unity UGUI 原理篇(二):Canvas Scaler 縮放核心

    https://blog.csdn.net/gz_huangzl/article/details/52484611 Canvas Scaler Canvas Scaler是Unity UI系統中,控制 ...

  4. DOTS默认情况下的性能

    利用Unity全新的高性能多线程数据导向技术堆栈(DOTS),充分利用当今的多核处理器.您的游戏运行速度更快,您的代码更易于在其他项目中阅读和重用. 重建Unity的核心 我们正在使用高性能多线程数据 ...

  5. 改写画质、突破性能, Unity 全面升级!

    技术变革,时代更迭.从<神庙逃亡>.<暗影之枪>等主流手游到独立联网的大型游戏,从绚丽多彩的影视动画到具备极致体验的运输建筑制造行业,从传统的2D 到立体3D 乃至沉浸式的VR ...

  6. Unity3d入门 - 关于unity工具的熟悉

    上周由于工作内容较多,花在unity上学习的时间不多,但总归还是学习了一些东西,内容如下: .1 根据相关的教程在mac上安装了unity. .2 学习了unity的主要的工具分布和对应工具的相关的功 ...

  7. 聊聊Unity项目管理的那些事:Git-flow和Unity

    0x00 前言 目前所在的团队实行敏捷开发已经有了一段时间了.敏捷开发中重要的一个话题便是如何对项目进行恰当的版本管理.项目从最初使用svn到之后的Git One Track策略再到现在的GitFlo ...

  8. Unity游戏内版本更新

    最近研究了一下游戏内apk包更新的方法. ios对于应用的管理比较严格,除非热更新脚本,不太可能做到端内大版本包的更新.然而安卓端则没有此限制.因此可以做到不跳到网页或应用商店,就覆盖更新apk包. ...

  9. Unity 序列化

    Script Serialization http://docs.unity3d.com/Manual/script-Serialization.html 自定义序列化及例子: http://docs ...

随机推荐

  1. 相比ICO,DAICO主要有这两方面优势

    都说ICO已死,很有一部分人对无币区块链持保留态度,自从V神提出DAICO一来,大家似乎看到了新的方向,不少项目围绕其展开.那对比ICO,DAICO有哪些优势呢?主要是以下两点: DAICO维护了投资 ...

  2. Machine Learning in Action(4) Logistic Regression

    从这节算是开始进入“正规”的机器学习了吧,之所以“正规”因为它开始要建立价值函数(cost function),接着优化价值函数求出权重,然后测试验证.这整套的流程是机器学习必经环节.今天要学习的话题 ...

  3. absolute布局的替代实现

    京东商城首页标价定位 小横条首页导航的下拉 1.京东商城首页标价定位 .p-img{ height: 130px;} .p-price{ margin:-28px 0 0 74px;} .price{ ...

  4. URAL - 1297 Palindrome —— 后缀数组 最长回文子串

    题目链接:https://vjudge.net/problem/URAL-1297 1297. Palindrome Time limit: 1.0 secondMemory limit: 64 MB ...

  5. 51nod 1040

    题目 题解:我们要求的是这个式子: $ \sum\limits_{i = 1}^n {\gcd (n,i)}  $ (下面式子中的d都是n的因子) 变形下  $ \sum\limits_{d = 1} ...

  6. 最近采集写的一个超简单实用的HTML解析类

    1. [文件] HtmlDom.php <?php$oldSetting = libxml_use_internal_errors( true ); libxml_clear_errors(); ...

  7. flash滑杆控制图片横向滚动

    flash滑杆控制图片横向滚动是一款FLASH动画图片左右滚动素材,滑杆控制滚动,效果很酷,带FLASH源文件. 下载:http://www.huiyi8.com/sc/9452.html

  8. linux命令学习笔记(24):Linux文件类型与扩展名

    Linux文件类型和Linux文件的文件名所代表的意义是两个不同的概念.我们通过一般应用程序 而创建的比如file.txt.file.tar.gz ,这些文件虽然要用不同的程序来打开,但放在Linux ...

  9. 解决系统存在大量TIME_WAIT状态的连接

    系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决, vi /etc/sysctl.conf 编辑文件,加入以下内容:net.ipv4.tcp_syncookies = 1net.ipv4 ...

  10. WPF 后台触发 Validate UI‘s Element

    wpf中有validateRule类, 用于界面元素的验证, 如何后台去控制validateRule呢? 1. UI层要binding写好的ValidateRule,分为Binding和MultiBi ...