我们最近发起了一次持续数年的改进行动,帮助用户更快地编写性能更高的代码,并带来长期的稳定性与兼容性。请在本文中了解我们在更新脚本基础技术栈方面所做的努力。
.NET生态正在多方面上积极动态地演变,而我们希望尽快将这些改进带给广大用户。我们内部的.NET技术小组正致力于不断改进.NET集成,更新最新的C#特性与.NET Standard 2.1。但根据大家的反馈,我们最近又在全面改善开发者体验上投入了更多力量。
本文将介绍我们正在努力解决的几个问题。相关话题已在GDC 2022的Unity Dev Summit上被讨论过。你可以在这里回顾完整演讲。
Unity与.NET的故事要从17年前说起,当时我们的CTO决定开始采用Mono .NET运行时和C#。Unity之所以偏爱C#,不仅是因为它很简单,还因为JIT(just-in-time)编译器可以将C#代码转译成效率较高的原生代码。而为了做到性能平衡、可控,Unity引擎剩下的大部分都采用了C++开发。
IL2CPP(Intermediate Language To C++) 是一种由Unity开发的脚本后端,可在为各种平台构建项目时替代Mono。使用IL2CPP构建项目时,Unity会在为所选平台创建本机二进制文件(例如.exe、apk、.xap)之前将脚本和程序集内的IL代码转换为C++。IL2CPP的一些用途包括提高Unity项目的性能、安全性和平台兼容性。
多年来,Unity一直依靠着Mono .NET运行时和C#语言(2.0)的一个特定分支来运行。在这期间,引擎对其他平台的支持也在不断扩增。我们还开发了自己的编译器和运行时:IL2CPP
,让你能够针对iOS及部分主机平台进行开发。
与此同时,整个微软.NET生态系统也在不断发展,并推出了新的许可和对非Windows平台的支持。我们借着这股东风在2018年升级到了Unity .NET Mono Runtime,并引入了更现代的C#版本(7.0以上)。同年,我们还发布了Burst编译器的首个版本,开创了为C#分支快速生成原生代码的先河。在取得这一突破后,Unity提出了一个新设想:将C#拓展到引擎剩余几个关键部分、不再用C++进行开发,并为DOTS运行时的开发做铺垫。
Unity 2020 LTS和Unity 2021 LTS带来了新版C#语言和.NET API(如Span)。同时,我们也看到.NET生态的性能有了极大的改善,其引入的csproj SDK风格和蓬勃发展的NuGet生态正在让开发环境变得更加友好。
经过长时间的发展,Unity已经包含了一个巨大的C++代码库,它继承了Mono .NET Runtime的职能来直接与.NET对象互动。这些代码对.NET (Core) Runtime来说不再合规或高效。
此外,Unity编辑器还绑定着一条复杂的自定义编译管线,它不依靠MSBuild运行,因此也不能轻易从引擎特性中获益。
在过去几年里,我们也一直在积极与广大用户交流,包括个人采访和Unity论坛,以发掘出真正能促使用户成功的改进。我们听到大家非常想使用最新的C#语言、.NET运行时技术和NuGet的第三方C#代码。在谈到Unity平台的使用时,大家都说希望借助高质量的C#测试、调试和剖析工具,标准.NET API和Unity API的优秀整合,来最大限度地挖掘出硬件的性能。 作为一名Unity C#程序员,你希望Unity工具能够与工具箱中的其他工具无缝衔接,能实现快速迭代,从而实现一流的运行时性能。
要达到这个目标,我们得花几年时间。我们将频繁更新博客和论坛,让大家了解我们在前进道路上遇到的技术挑战。
这项计划的第一步是在Unity内部集结所有热衷于C#和.NET的内部员工,建立一支C#/.NET技术小组来开展工作。
我们的工作将建立在.NET生态系统之上,而非开发定制解决方案。为了让用户能享受新版.NET SDK/Runtime和MSBuild所带来的性能与生产力提高,我们将从Mono .NET Runtime转移到CoreCLR,即现代的.NET (Core) Runtime。
这项行动也会带来当前.NET领域以外的创新,让C#脚本能更快地完成.NET迭代。我们将致力于结合IL2CPP和Burst这两个JIT和AOT (ahead-of-time) 解决方案,在编译效率和CodeGen(代码生成)质量之间的达到最佳平衡。
在外部,我们将与微软、JetBrains等业内伙伴合作,保证Unity创作者能用上最新的.NET技术。我们也在进一步深入参与开源社区。 这项工作将分成几个步骤进行。再来看看我们未来的计划。
今年,我们的团队计划在以下几个方面开展工作。
迭代时间仍然是我们首要的工作重点,我们清楚每一名用户都想尽可能地利用起自己的时间。以下是我们改进工作的几个例子。
我们正在改进编译管线的IL Post Processing所耗费的时间,它负责在C#被编译后修改编译好的.NET程序集。现在IL Post Processing将在编译阶段后持续运行,来降低约几百毫秒的耗时。
随着Burst编译器的使用愈加频繁,我们将采用一种可传递哈希算法来改进代码改动检测的精细度。如此一来,我们就能够找出那些需要快速编译的可爆发(Burstable)代码。我们正在将Burst编译器移出进程,使其能在单独的.NET 6.0可执行程序中运行,更快地完成代码编译。
我们还将改进引擎每次调用TypeCache、在后台建立镜像数据(reflection data)的过程,此改进域的重新加载。
我们还将添加测试和验证流程来更好地跟踪软件包和项目模板的迭代时间倒退。
至于转移到MSBuild,我们第一步须将编译管线与Unity编辑器分割开来,并将其转移到一个单独的进程中。这个过程涉及多年以来遗留下来的成千上万行C++和C#代码,我们要实现这一目标就必须解开这些代码——同时还要保持向下兼容。这项工作并不会在用户的角度带来多大变化,但它将铺好通往MSBuild的道路并简化引擎的维护。
我们还将改进Burst的C# IDE调试体验,推出一种新的调试模式,当用户在Burst代码路径上设置断点时,自动将调试器切换到托管调试。这意味着你不必再手动删去调试路径上的[BurstCompile]
属性。
转移至.NET CoreCLR运行时的工作已经开始,这是一个非常具有挑战性的旅程。为了使整个过程能够顺利完成,我们将分步骤解决各个问题,并在保证现有Unity项目稳定的前提下碎片化发布更新。
因此,整个迁移过程将分多个阶段完成:
首先,我们将为桌面平台上的独立运行版提供.NET CoreCLR的支持。该运行时将和现有的Mono与IL2CPP后端一起在运行版设置中列出。 第一阶段我们将完成Unity引擎核心部分(比编辑器部分小得多)的迁移,并尽量解决迁移过程所涉及的绝大部分技术挑战。我们的目标是在2023年期间发布这个新运行时,目前你仍需用.NET Standard 2.1 API访问.NET运行时。
然后,我们会把Unity编辑器移植到.NET CoreCLR,同时移除对.NET Mono运行时的支持。第二阶段我们将挑战不使用AppDomains
在编辑器内重新加载脚本,并完成向.NET CoreCLR转移。这一阶段也将涉及到升级IL2CPP、支持dotnet/runtime仓库的基础类库。你将能使用完整的.NET 7.x或8.0 API。我们希望能在2024年里发布这个新的编辑器。
Unity 2021 LTS新支持的.NET Standard 2.1使我们能够从多个方面着手现代化Unity运行时。我们目前正在推进两项改进工作。
改进async/await编程模型。基础性的async/await编程方法主要用于编写必须异步完成、不阻塞引擎主循环的游戏代码。
2011年,在async/await成为.NET的主流之前,Unity引入了基于迭代器的异步运算协程(coroutines),但这种方法并不兼容async/await,并且效率也更低。同时,.NET Standard 2.1一直在改进C#和.NET对async/await的支持,推出了ValueTask来更高效地完成async/await的运算,并允许用户自行用AsyncMethodBuilder编写的类任务系统。
在有了这些改进之后,我们将努力结合async/await与Unity中现有的异步操作(如等待下一帧或等待UnityWebRequest的完成)。第一步,我们将引入取消令牌(cancellation token),改进MonoBehavior被销毁时或退出运行模式时被挂起异步任务的取消操作。我们也一直在与UniTask作者等社区贡献者密切合作,保证他们能够用上这些新功能。
利用Span减少内存分配和拷贝。Unity本身是一个带有C#编程外壳的C++引擎,两种语言之间存在着大量的数据交换。这就造成引擎经常性地来回复制数据、分配托管对象,造成工作效率低下。
C# 7.2引入的Span可以有效改善这个问题,且.NET Standard 2.1默认可使用Span值类型。近年来,你可能听说过或读到过许多归功于Span的.NET运行时重大性能改进(改进细节可在.NET Core 2.1、.NET Core 3.0、.NET 6、.NET 6等博文中了解)。我们同样希望在Unity中利用起它,并有效地减少分配,从而减少Garbage Collection卡顿、提高大量API的整体性能。
相信大家和我们一样对这些变化和功能感到非常兴奋。
https://unity.com/roadmap/unity-platform/engineering
欢迎大家来论坛留下自己的想法。我们将定期更新Unity路线图的引擎工程部分,你可以在那里与我们分享你的功能需求和优先事项建议。
Span<T>
和Memory<T>
编写高性能代码