资讯
展览资讯 大型展会 灯光节 大型盛典 赛事 中标捷报 产品快讯 热门话题 艺术节 活动 演出 新闻 数艺报道 俱乐部签约
观点
大咖专访 观点洞察 书籍推荐 吐槽 设计观点 企业访谈 问答 趋势创新 论文 职场方法 薪资报价 数艺专访
幕后
幕后故事 团队访谈 经验分享 解密 评测 数艺访谈
干货
设计方案 策划方案 素材资源 教程 文案资源 PPT下载 入门宝典 作品合集 产品手册 电子书 项目对接
  • 0
  • 0
  • 0

分享

Unity 2021.2功能亮点 | 加快脚本运行速度,IL2CPP运行时的性能改进

原创 2022-04-06

热知识:在 Unity 里码代码时,你可以利用 C# 的可读性和安全性最大程度地提高项目在目标平台上运行时的性能。为了进一步提高性能,Unity 的 .NET Tech Group 一直在更新 C# 脚本背后的基础技术栈。


在 Unity 2021 中,我们推出了几项可以加快 IL2CPP 脚本后端运行速度的改进。本文中,让我们来仔细了解几个可明显加快脚本运行速度的关键改动。


IL2CPP:

https://docs.unity3d.com/2021.2/Documentation/Manual/IL2CPP.html


委托(Delegate)的调用


作为 C# 的一种特色功能,委托(Delegate)的 CreateDelegate API 有点太过复杂。灵活性是委托的一大特性:它可以是开放的,也可以是封闭的,可以用虚拟或接口调用来调用实例或静态方法,甚至泛型方法和泛型类方法的区别!由于有这么多种用法,项目运行时要想保证调用和被调用的方法都正确,就必须进行多次检查。


CreateDelegate API:

https://docs.microsoft.com/en-us/dotnet/api/system.delegate.createdelegate?view=net-5.0


在 Unity 2021.2 里,IL2CPP 将在编译时预先验证所有的调用类型。意味着开放委托需要两条间接调用指令实现调用,而封闭委托只需一条。这使委托的调用能与 .NET Framework 和 .NET Core 的调用相同。


得益于此,委托现在的调用要比以往更快捷,在部分基准测试下的速度也要快得多。



多余的装箱(Boxing)检测


C# 的装箱(Boxing)是一种将特定类型的值打包成 System.Object 对象的过程。这一过程涉及到在内存堆上分配存储空间,因此也是一个较缓慢的过程。为了进一步加快 IL2CPP 运行时的运行速度、挤出更多性能,我们在 Unity 2021.2 中取消了部分 boxing 操作。


装箱(Boxing):

https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing


这就要求可空类型须以特定的方式进行装箱,让运行时能在每次装箱后仍能识别可空类型。通过取消这些非必要的检测,装箱的性能(尤其在可空类型与泛型上)将有不小的提高。



虚拟泛用方法的调用


虚拟泛用方法是 C# 的特色表达,其应用较为困难。不同于直接调用,虚拟泛用方法在构建时并不会指明调用的目标,而是在运行时另行确定。


为了提高虚拟泛用方法和接口调用的性能,我们在 Unity 2021.2 中做出了一定的改进,如以下代码所示:



interface Interface { T GetValue(); } class Base : Interface { public virtual T GetValue() { return default(T); } } class Derived : Base { public override T GetValue() { return default(T); } } private Base obj = new Derived(); private Interface iface = obj; public void CallToVirtualGenericMemberFunction() { obj.GetValue(); } public void CallToGenericInterfaceMemberFunction() { iface.GetValue(); }

右滑查看完整代码



Enum.HasFlag


你可以为 C# 枚举类型加上[Flags]特性来组成多种选项组合。Enum.HasFlag 方法常用于在 flags 枚举中检测特定值。在 Unity 2021.2 中,该方法的调用速度被提高 100 倍以上。


[Flags]特性:

https://docs.microsoft.com/en-us/dotnet/api/system.flagsattribute?view=net-5.0


Enum.HasFlag:

https://docs.microsoft.com/en-us/dotnet/api/system.enum.hasflag?view=net-5.0


像下方的这个基准测试:


public void CallToEnumHasFlag() {_enum.HasFlag(_flag); }


会将以下生成代码...



IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void EnumHasFlag_CallToEnumHasFlag_m5819FE655D569D7AF856B879164E6416EFEFC30E (EnumHasFlag_t72757859AA4C348BBEE0A64FDBB747AFCFE326C2 * __this, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&MyEnum_t67450C4DBC081C689DA95C351B619398929DC7A1_il2cpp_TypeInfo_var); s_Il2CppMethodInitialized = true; }{ int32_t L_0 = __this->____enum_0; int32_t L_1 = L_0; RuntimeObject * L_2 = Box(MyEnum_t67450C4DBC081C689DA95C351B619398929DC7A1_il2cpp_TypeInfo_var, &L_1); int32_t L_3 = __this->____flag_1; int32_t L_4 = L_3; RuntimeObject * L_5 = Box(MyEnum_t67450C4DBC081C689DA95C351B619398929DC7A1_il2cpp_TypeInfo_var, &)L_4); NullCheck((Enum_t2A1A94B24E3B776EEF4E5E485E290BB9D4D072E2 *)L_2); bool L_6;L_6 = Enum_HasFlag_m15293B523AA7BA15272699C7304E908106AD7F7B((Enum_t2A1A94B24E3B776EEF4E5E485E290BB9D4D072E *)L_2, (Enum_t2A1A94B24E3B776EEF4E5E485E290BB9D4D072E2 *)L_5, NULL) ; return; }}

右滑查看完整代码


转换成以下代码。



IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void EnumHasFlag_CallToEnumHasFlag_m5819FE655D569D7AF856B879164E6416EFEFC30E (EnumHasFlag_t72757859AA4C348BBEE0A64FDBB747AFCFE326C2 * __this,const RuntimeMethod* method) { { int32_t L_0 = __this->____enum_0;int32_t L_1 = L_0; int32_t L_2 = __this->____flag_1; int32_t L_3 = L_2; bool L_4 = il2cpp_codegen_enum_has_flag(L_1, L_3) ; return; }}

右滑查看完整代码


它将删去非必要的装箱和可空类型检测,让 C++ 编译器能够更好地优化代码。在这个小基准测试里,代码的运行时间从 162 纳秒缩短成了 1.18 纳秒。



限制性调用


限制性调用是 C# 的另一个实用功能。它们能以更低的性能成本完成虚拟方法的调用等常被认为过于繁重的操作。这是怎么做到的呢? 


它们本质上是一种调用方式的“提示”。IL2CPP 现在支持抓取更多“提示”,将繁重的调用转换成轻便的直接调用。 


假设代码中有一个这样的值类型:


private struct SimpleValueType { }


以及另一个可在任意类型上调用 System.Object 下 Equals 虚拟方法的通用方法:


private static void Equals(T t) {t.Equals(null); }


然后,类似下方的基准测试...


public void CallToEqualsValueType() {Equals(_simpleValueType); }


...将以快 10 倍的速度完成。


因为 IL2CPP 得知自己无须调用虚拟方法,而是可以直接进行调用。如你所见,限制性调用在基准测试中表现出了极大的改善。



快来试试吧


欢迎大家使用 Profiler 来详细了解自己的脚本。


请注意:文中所有的改进数据皆以基准测试中的表现为基准,在其他项目中可能会有出入。


Profiler:

https://docs.unity3d.com/Manual/Profiler.html


长按关注

Unity 官方微信

第一时间了解Unity引擎动向,学习最新开发技巧

阅读原文

* 文章为作者独立观点,不代表数艺网立场转载须知

本内容由数艺网主动采集收录,信息来源为 “Unity官方平台” 公开网络发布内容。第三方如需转载本内容,必须完整标注原作者信息及 “来源:数艺网”,严禁擅自篡改、删减或未标注来源转载。 并附上本页链接: 若您的内容不希望被数艺网收录,或认为此举侵犯了您的合法权益,敬请通过微信 ID:d-arts-cn 联系数艺网。我们将致以诚挚歉意,并第一时间为您办理下架或删除处理。

数字媒体艺术 新媒体艺术 科技艺术 作品分享 科技前沿

20902 举报
  0
登录| 注册 后参与评论