
这是对我们新的面向数据技术栈(
DOTS )的简要介绍。 我们将分享一些见解,以帮助您了解Unity如今的发展方式和原因,并告诉您我们计划朝哪个方向发展。 将来,我们计划在Unity博客的DOTS博客上发布新文章。
让我们谈谈C ++。 这是现代Unity编写的语言。
游戏开发人员必须以一种或多种方式处理的最复杂的问题之一是:程序员必须向可执行文件提供目标处理器可以清除的指令,并且当处理器执行这些指令时,游戏应该启动。
在对性能敏感的代码部分中,我们预先知道最终指令应该是什么。 我们只需要一种简单的方法,使我们能够一致地描述我们的逻辑,然后检查并确保生成了我们所需的指令。
我们认为C ++语言对于此任务不是很好。 例如,我希望对循环进行矢量化处理,但是可能有百万个原因导致编译器无法对其进行矢量化处理。 由于某些看似微不足道的变化,今天要么被矢量化,要么明天不被矢量化。 甚至很难确保我所有的C / C ++编译器都可以对我的代码进行矢量化处理。
我们决定开发自己的“非常方便的生成机器代码的方式”,以满足我们的所有愿望。 可能会花费大量时间将C ++设计的整个过程朝着我们需要的方向稍微弯曲一点,但我们认为,将精力投入到开发能够完全解决我们面临的所有设计问题的工具链上将更为合理。 我们将在开发时考虑到游戏开发人员必须解决的任务。
我们优先考虑哪些因素?
- 性能=正确。 我应该能够说:“如果由于某种原因该循环未向量化,那么它一定是编译器错误,而不是类别中的情况”。真正的价值,经营业务!”
- 跨平台。 无论目标平台是iOS还是Xbox,我编写的输入代码都应该完全相同。
- 我们应该有一个整洁的迭代循环,当我更改源代码时,我可以轻松地看到为任何体系结构生成的机器代码。 当您需要了解所有这些机器指令的功能时,机器代码“查看器”将对培训/说明大有帮助。
- 安全性 通常,游戏开发人员不会在其优先级列表中将安全放在首位,但是我们认为Unity的最酷功能之一是确实很难破坏其中的内存。 应该有一种运行任何代码的模式-并且我们明确地修复了一个错误,该错误使大字母显示有关此处发生的事情的消息:例如,我在读取/写入或尝试取消引用零时超出了限制。
因此,在弄清对我们来说重要的是什么之后,让我们继续下一个问题:用哪种语言编写可从中生成此类机器代码的程序更好? 假设我们有以下选项:
什么,C#? 对于我们的内部循环谁的性能尤为关键? 是的 C#是一个完全自然的选择,在Unity的上下文中有很多非常好的东西:
- 这是我们的用户已经使用的语言。
- 它具有出色的IDE,可用于编辑/重构和调试。
- 已经有一个将C#转换为中间IL的编译器(我们正在谈论Microsoft的C#的Roslyn编译器),您可以简单地使用它而不是自己编写。 我们拥有将中间语言转换为IL的丰富经验,因此我们只需要执行代码生成并对特定程序进行后处理即可。
- C#避免了许多C ++问题(由于包含标头,PIMPL模式,编译时间长)
我自己真的很喜欢用C#编写代码。 但是,就性能而言,传统的C#不是最好的语言。 在过去的几年中,C#开发团队,负责标准库和运行时的团队在这一领域取得了长足的进步。 但是,在使用C#时,无法精确控制数据在内存中的位置。 而这正是我们需要解决以提高生产率的问题。
另外,该语言的标准库围绕“堆上的对象”和“具有指向其他对象的指针的对象”组织。
同时,使用对性能至关重要的代码片段,您几乎可以完全不用标准库(再见Linq,StringFormatter,List,Dictionary),禁止选择操作(=无类,仅结构),反射,禁用垃圾收集器和虚拟操作调用,并添加一些允许使用的新容器(NativeArray和company)。 在这种情况下,C#语言的其余元素看起来非常好。 请参阅Aras博客以获取示例,其中描述了一个临时路径跟踪器项目。
这样的子集将帮助我们轻松处理热循环中所有相关的任务。 由于这是C#的完整子集,因此可以像常规C#一样使用它。 尝试访问时,我们会收到与出国相关的错误,我们将获得出色的错误消息,我们将提供调试器支持,并且编译速度是如此之快,以至于您已经忘了使用C ++了。 我们通常将此子集称为高性能C#或HPC#。
突发编译器:今天怎么办?
我们编写了一个称为Burst的代码生成器/编译器。 在
Unity版本
2018.1和更高版本中,它作为“预览”模式下的软件包提供。 他还有很多工作要做,但今天我们对他感到满意。
有时候,我们通常比C ++更快地工作-仍然比C ++慢。 第二类包括性能缺陷,我们相信这些缺陷将能够解决。
但是,仅比较性能是不够的。 同样重要的是需要做什么来实现这种性能。 示例:我们从当前的C ++渲染器中获取了剔除代码,并将其移植到Burst。 性能没有改变,但是在C ++版本中,我们必须做出令人难以置信的平衡,才能说服C ++编译器进行矢量化。 带Burst的版本紧凑了大约四倍。
老实说,乍看之下“您应该重写对C#性能至关重要的代码”的整个故事并没有吸引内部Unity团队的任何人。 对于我们大多数人来说,这听起来像是“更接近硬件!”使用C ++时。 但是现在情况发生了变化。 使用C#,我们可以完全控制从编译源代码到生成机器代码的整个过程,如果我们不喜欢任何细节,只需将其修复即可。
我们将缓慢但确定地将所有对性能至关重要的代码从C ++移植到HPC#。 使用这种语言,可以更轻松地实现我们所需的性能,更难编写错误,并且更易于使用。
这是Burst Inspector的屏幕截图,您可以在其中轻松查看为各种热循环生成的汇编指令:

Unity有许多不同的用户。 他们中的一些人可以从内存中回忆起整个arm64指令集,而其他人甚至可以在没有计算机科学博士学位的情况下就充满热情地创建。
当它加速执行引擎代码所花费的帧时间的一部分(通常为90%以上)时,所有用户都会获胜。 实际上,随着Asset Store软件包的作者采用HPC#,使用Asset Store软件包的可执行代码的份额实际上正在加速增长。
高级用户还可以从HPC#上编写自己的高性能代码这一事实中受益。
点优化
在C ++中,很难让编译器在优化项目的不同部分中的代码时做出不同的折衷决定。 您可以指望的最详细的优化是优化级别的逐个文件指示。
设计Burst是为了使您可以接受该程序的唯一方法作为输入,即:热循环的入口点。 Burst会编译此函数及其调用的所有内容(必须保证事先知道这些被调用的元素:我们不允许使用虚函数或函数指针)。
由于Burst仅在程序的一小部分上运行,因此我们将优化级别设置为11。Burst几乎嵌入了每个呼叫站点。 删除if-check,否则将不会删除它,因为在嵌入式形式中,我们可以获得有关函数参数的更完整信息。
它如何帮助解决常见的线程问题?
C ++(以及C#)并不能特别帮助开发人员编写线程安全的代码。
即使在今天,在典型的游戏处理器开始配备两个或多个内核十多年之后,编写有效使用多个内核的程序还是非常困难的。
数据竞速,不确定性和死锁是使编写多线程代码如此困难的主要挑战。 在这种情况下,我们需要“确保此函数及其调用的所有内容永远不会开始读取或写入全局状态”类别中的功能。 我们希望所有违反此规则的行为都会导致编译器错误,而不是保留“我们希望所有程序员都遵守的规则”。 突发引发编译错误。
我们强烈建议Unity用户(并且在他们的圈子中保持一致)编写代码,以便将其中计划的所有数据转换分为任务。 每个任务都是“功能性”的,并且副作用是免费的。 它明确指示必须使用的只读缓冲区和读/写缓冲区。 任何尝试访问其他数据的操作都会导致编译错误。
任务计划程序确保在您的任务运行时,不会有人写入您的只读缓冲区。 并且我们保证在任务执行期间不会有人从您的缓冲区中读取数据,而该缓冲区是为读写而设计的。
每当您分配违反这些规则的任务时,您都会收到编译错误。 不仅发生在不幸的比赛条件下。 该错误消息将说明您正在尝试分配一个应从缓冲区A读取的任务,但是之前您已分配了一个将写入A的任务。因此,如果您确实要执行此操作,则必须将前一个任务指定为依赖项。
我们相信,这种安全机制有助于在修复许多错误之前就将它们捕获,从而确保有效利用所有内核。 不可能引起比赛条件或僵局。 无论您有多少个线程,或由于某个其他进程的干预而使一个线程中断多少次,结果都保证是确定的。
掌握整个堆栈
当我们深入了解所有这些组件时,我们还可以确保它们彼此了解。 例如,向量化失败的一个常见原因是:编译器无法保证两个指针不会指向相同的存储点(别名)。 我们知道两个NativeArray绝不会像这样重叠,因为它们已经编写了一个集合库,并且我们可以在Burst中使用此知识,因此我们不会因为害怕将两个指针指向一个而拒绝优化。相同的记忆。
同样,我们编写了
Unity.Mathematics数学库。 众所周知,“爆发”是“彻底的”爆发(将来),在math.sin()之类的情况下,它可以指出选择退出优化。 由于对于Burst math.sin()不仅是需要编译的普通C#方法,它还将了解sin()的三角性质,因此它将了解sin(x)== x对于小的x值(Burst可以独立证明),将理解它可以用泰勒级数的扩展代替,从而部分地降低了精度。 将来,Burst还计划以浮点数实现跨平台和设计确定性-我们相信这样的目标是可以实现的。
游戏引擎代码和游戏代码之间的差异变得模糊
当我们用HPC#编写Unity运行时代码时,游戏引擎和游戏本身都是用相同的语言编写的。 我们可以分发转换为HPC#的运行时系统作为源代码。 每个人都可以向他们学习,提高他们,适应他们自己。 我们将拥有一定程度的竞争环境,没有什么可以阻止我们的用户编写比我们编写的更好的粒子系统,游戏物理或渲染器。 通过使我们的内部开发过程更接近于用户开发过程,我们还可以感觉到用户的感觉更好,因此,我们将尽一切努力构建一个单一的工作流程,而不是两个不同的工作流程。