[Unity ECS] 在 DOTS 中的: C++ & C#

63 篇文章 11 订阅

英文原文:

https://blog.unity.com/technology/on-dots-c-c

  这是对我们新的面向数据的技术堆栈 (DOTS) 的简要介绍,分享一些关于我们如何以及为什么达到今天的位置以及下一步要去哪里的见解。我们计划在不久的将来在这个博客上发布更多关于 DOTS 的信息。

  让我们来谈谈C++。今天,Unity是用这种语言编写的。

  许多高级游戏程序员最后的问题之一是,他们需要提供一个目标处理器可以理解的指令的可执行文件,执行后就可以运行游戏。

  对于我们代码的性能关键部分,我们知道我们希望最终的指令是什么。我们只是想用一种简单的方法来合理地描述我们的逻辑,然后相信并验证所生成的指令是我们想要的。

  在我们看来,C++在这项任务上并不出色。我想让我的循环被矢量化,但是有无数的事情可能会发生,从而使编译器不对它进行矢量化。今天它可能被矢量化了,但如果明天发生了一个新的看似无辜的变化,它就不会被矢量化。要说服我所有的C/C++编译器对我的代码进行矢量化是很难的。

  我们决定做我们自己的 “合理舒适的机器代码生成方式”,它能满足我们关心的所有条件。我们可以花很多精力去尝试将C++的设计训练对我们更有利的方向,但我们更愿意将这些精力花在一个我们可以做所有设计的工具链上,而且我们正是为游戏开发者的问题而设计。

我们关心哪些方面?
  • 性能就是正确性。我应该能够说:“如果这个循环由于某种原因不能矢量化,那应该是一个编译器错误,而不是’哦,代码现在只是慢了8倍,但它仍然产生正确的值,没什么大不了的!’”

  • 跨架构。我写的输入代码不应该在我针对iOS时和针对Xbox时有所不同。

  • 我们应该有一个很好的迭代循环,当我改变我的代码时,我可以很容易地看到为所有架构生成的机器代码。机器代码 "查看器 "应该能很好地教导/解释所有这些机器指令的作用。

  • 安全性。大多数游戏开发者对安全问题的重视程度并不高,但我们认为,在Unity中真的很难出现内存损坏,这已经是它的杀手锏之一。应该有一种模式,我们可以在其中运行这段代码,如果我读/写出界或取消引用null,会给我们一个明确的错误信息。

  好的,既然我们知道我们关心什么,下一步就是决定这个机器代码生成器的输入语言是什么。假设我们有以下选项:

  • 自定义语言
  • C 或 C++ 的一些改编/子集
  • C# 的子集

  说什么C#?对于我们最关键的性能的内循环?是的。C#是一个非常自然的选择,它为Unity带来了很多好处。

  • 这是我们的用户今天已经使用的语言。
  • 具有出色的 IDE 工具,包括编辑/重构以及调试。
  • 一个 C#->中间 IL 编译器已经存在了(微软的Roslyn C#编译器),我们可以直接使用它而不必自己编写。
  • 我们有很多修改mediate-IL的经验,所以很容易对实际的程序进行codegen和postprocessing。
  • 避免了许多C++的问题(头文件包含地狱,PIMPL模式,漫长的编译时间)。

  我自己相当喜欢用C#写代码。然而,从性能的角度来看,传统的C#并不是一种了不起的语言。在过去的两年里,C#语言团队、标准库团队和运行时团队都取得了很大的进步。但是,当使用C#语言时,你无法控制你的数据在内存中的位置/方式。而这恰恰是我们需要提高性能的地方。

  除此之外,标准库是围绕着 "堆上的对象 "和 "具有指向其他对象的指针引用的对象"展开的。

  也就是说,在处理一段性能关键代码时,我们可以放弃大部分标准库,(再见 Linq、StringFormatter、List、Dictionary)、不允许分配(等于没有类,只有结构)、反射、垃圾收集器和虚拟调用,并添加一些允许使用的新容器(NativeArray 和一些类似的容器)。然后,C# 语言的其余部分看起来非常好。查看 Aras 的博客,了解他的路径跟踪器玩具项目中的一些示例。

  这个子集可以让我们在热循环中舒适地做我们需要的一切。因为它是C#的一个有效子集,我们也可以把它当作普通的C#来运行。我们可以在越界访问时出错,有很好的错误信息、调试器支持和你忘了在C++中工作时可能出现的编译速度。我们经常把这个子集称为高性能C#或HPC#。

Burst 编译器:我们今天在哪个阶段了?

  我们已经建立了一个名为Burst的代码生成器/编译器。它从Unity 2018.1开始作为预览包提供。我们还有很多工作要做,但我们今天已经对它很满意了。

  我们有时比C++快,也有时比C++慢。后一种情况我们认为是我们有信心可以解决的性能错误。

  但只比较性能是不够的。同样重要的是,你必须做什么才能获得这种性能。例如:我们把目前C++渲染器的C++删减代码移植到了Burst上。性能是一样的,但是C++版本不得不做一些不可思议的体操来说服我们的C++编译器真正进行矢量化。而Burst版本则小了4倍。

  说实话,"你应该把你最关键的性能代码转移到C#"的故事也没有导致Unity内部的每个人立即买账。对于我们大多数人来说,使用C++时感觉 “你更接近metal”。但这种情况不会持续很久。当我们使用C#时,我们可以完全控制从源码编译到机器代码生成的整个过程,如果有不喜欢的东西,我们只需进入并修复它。

  我们将缓慢但肯定地把我们在C++中的每一段性能关键代码移植到HPC#中。它更容易获得我们想要的性能,更难写出bug,也更容易工作。

  这是Burst Inspector的截图,让你轻松地看到为你的不同Burst热循环产生了什么装配指令。
在这里插入图片描述
  Unity 有很多不同的用户。有些人可以从内存中枚举整个 arm64 指令集,有些人则乐于在没有获得计算机科学博士学位的情况下创造东西。

  所有的用户都会受益,因为他们的框架时间中用于运行引擎代码的部分(通常是90%以上)会变快。随着Asset Store package作者采用HPC#,运行Asset Store package运行时代码的部分也变得更快。

  高级用户在此基础上还可以用HPC#编写自己的高性能代码,从而获益匪浅。

优化粒度

  在C++中,很难要求编译器为你项目的不同部分做出不同的优化权衡。你所拥有的最好的东西就是在指定优化级别时的每个文件的粒度。

  Burst被设计为接受该程序中的一个方法作为输入:热循环的入口点。它将编译该函数和它所调用的一切(保证是已知的:我们不允许虚拟函数或函数指针)。

  因为Burst只对程序的一个相对较小的部分进行操作,我们将优化级别设置为11。Burst内联了几乎所有的调用站点。删除否则不会被删除的if检查,因为在内联形式下,我们有更多关于函数参数的信息。

这如何帮助解决常见的多线程问题

  C++(也不是 C#)在帮助开发人员编写线程安全代码方面没有多大作用。

  即使在游戏消费硬件拥有>1个内核的十多年后的今天,也很难有效使用多个内核的程序。

  数据竞争、非确定性和死锁都是使多线程代码难以交付的挑战。我们想要的是像 "确保这个函数和它所调用的一切都不会读写全局状态 "这样的功能。我们希望对该规则的违反是编译器错误,而不是 “我们希望所有程序员都遵守的准则”。Burst给出了一个编译器错误。

  我们鼓励Unity用户和我们自己编写 "Job化 "的代码:将所有需要发生的数据转换分割成Job。每个Job都是 “功能性的”,也就是没有副作用的。它明确指定了它所操作的只读缓冲区和读/写缓冲区。任何访问其他数据的尝试都会导致编译器错误。

  Job调度器将保证在你的Job运行时没有人向你的只读缓冲区写东西。我们也会保证在你的Job运行时,没有人从你的读/写缓冲区中读出。

  如果你安排的Job违反了这些规则,你每次都会得到一个运行时错误。错误信息会解释说,你试图安排一个要从缓冲区A读取的Job,但你之前已经安排了一个要写到A的Job,所以如果你想这样做,你需要把之前的那个Job作为一个依赖项。

  我们发现这种安全机制在提交之前就抓住了很多bug,并导致了所有内核的有效使用。不可能出现死锁或竞争条件的代码。无论有多少个线程在运行,或一个线程被其他进程打断多少次,结果都保证是确定性的。

黑掉整个栈

  通过对所有这些组件进行Hack,我们可以使它们相互认识。例如,矢量化没有发生的一个常见情况是,编译器不能保证两个指针不指向同一个内存(别名)。我们知道两个NativeArray永远不会别名,因为我们写了集合库,我们可以在Burst中使用这个知识,这样它就不会因为担心两个数组指针会指向相同的内存而放弃优化了。

  同样地,我们编写了Unity.Mathemetics数学库。Burst对它有很深的了解。它将(在未来)能够对math.sin()这样的东西进行牺牲精度的优化。因为对Burst来说,math.sin()并不是任何可以编译的C#方法,它将理解sin()的三角函数属性,理解sin(x)==x的小值(Burst可能会证明这一点),理解它可以被泰勒级数扩展所取代,从而牺牲一定的精度。跨平台和架构的浮点确定性也是Burst的一个未来目标,我们认为是可以实现的。

引擎代码和游戏代码之间的区别消失了

  通过用HPC#编写Unity的运行时代码,引擎和游戏是用同一语言编写的。我们将把转换为HPC#的运行时系统作为源代码发布。每个人都将能够从他们身上学到东西,改进他们,为他们量身定做。我们将有一个公平的竞争环境,没有什么可以阻止用户写出比我们更好的粒子系统、物理系统或渲染器。我希望很多人会这样做。通过让我们的内部开发过程更像用户的开发过程,我们也会更直接地感受到用户的痛苦,我们可以把所有的精力集中在改进一个工作流程上,而不是两个不同的工作流程。

在我的下一篇文章中,我将介绍 DOTS 的不同部分:实体组件系统。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值