我们将推出三篇系列文章介绍IL2CPP AOT编译器的小优化,帮助大家更好的理解代码执行的过程。本文为系列第一篇。

Unity的脚本虚拟机团队一直在寻找让代码更快运行的方式。本系列文章将为大家介绍IL2CPP AOT编译器的小优化,并教大家如何利用它们。尽管这些小优化并不能让您的代码运行速度提高2-3倍,但它们会在游戏中起到重要作用,我们希望这些优化能让您对代码的执行方式有更深入的理解。

现代编译器都很擅于执行一些优化来提高代码的运行时性能。作为开发者,我们经常可以通过将已了解功用的代码,通过一定信息明确传达给编译器,进而帮助编译器提高效率。本文将详细讲解一个关于 IL2CPP的小优化,并看看它将如何提高您现有代码的运行效率。

去虚拟化

众所周知,虚方法的调用通常比直接调用的消耗更大。我们一直致力于改善libil2cpp运行库的性能以降低虚方法调用的消耗(下篇文章将介绍更多该内容),但某些排序算法仍然需要一些运行时的查找。编译器无法知道哪些方在运行时被调用,亦或可不可以被调用?

去虚拟化是一个很常见的编译器优化策略,也就是将虚方法调用改为直接的方法调用。当编译器编译时可以提供准确的“实际”方法时,这个策略就会被启用。不幸的是,这点往往很难做到,因为编译器通常无法看到整个代码库。然而,如果可以的话,这将使虚方法的调用更快速。

典型例子

作为一个年轻的开发者,我学习虚方法是从一个相当惯例的动物示例开始的。这个例子您可能也很熟悉:

[C#] 纯文本查看 复制代码public abstract class Animal { public abstract string Speak(); } public class Cow : Animal { public override string Speak() { return Moo; } } public class Pig : Animal { public override string Speak() { return Oink; } }

在 Unity (版本 5.3.5) 中,我们可以使用这些类来做个小农场:

[C#] 纯文本查看 复制代码public class Farm: MonoBehaviour { void Start () { Animal[] animals = new Animal[] {new Cow(), new Pig()}; foreach (var animal in animals) Debug.LogFormat(Some animal says {0}, animal.Speak()); var cow = new Cow(); Debug.LogFormat(The cow says {0}, cow.Speak()); } }

这里每次调用Speak的都是虚方法调用。让我们看看 IL2CPP去虚拟化这些方法调用是如何提高性能的。

生成的C++代码还不赖

我比较喜欢IL2CPP的一个功能是它会生成C++代码而非汇编代码。当然,这些生成的代码与一般手写的看起来并不一样,但这种代码要比汇编代码容易理解得多。下面看看生成的 foreach循环体代码:
[C#] 纯文本查看 复制代码// Set up a local variable to point to the animal array //设置一个本地的变量指向animal(动物)队列 AnimalU5BU5D_t2837741914* L_5 = V_2; int32_t L_6 = V_3; int32_t L_7 = L_6; // Get the current animal from the array // 从队列中获得当前的animal(动物) V_1 = ((L_5)->GetAt(static_cast<il2cpp_array_size_t>(L_7))); Animal_t3277885659 * L_9 = V_1; // Call the Speak method //调用Speak方法 String_t* L_10 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Animal::Speak() */, L_9);

这里移除了一些生成的代码来简化其内容。看到那个丑陋的Invoke调用了么?它将会在 vtable中查找适当的虚方法并进行调用。vtable的查找会比直接的函数调用慢一些,但这可以理解。因为 Animal(动物)可能是 Cow(牛)或者Pig(猪),或者一些的派生类型。

下面看看生成的代码中的第二个调用Debug.LogFormat,这个看起来更像是直接调用:

[C++] 纯文本查看 复制代码// Create a new cow // 创建一个新的cow(牛) Cow_t1312235562 * L_14 = (Cow_t1312235562 *)il2cpp_codegen_object_new(Cow_t1312235562_il2cpp_TypeInfo_var); Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL); V_4 = L_14; Cow_t1312235562 * L_16 = V_4; // Call the Speak method // 调用Speak(叫)函数 String_t* L_17 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Cow::Speak() */, L_16);

这个例子中仍然使用的是虚方法调用! 事实上IL2CPP对于优化非常保守,绝大多数情况下其优先确保正确性。由于它并未对整个项目做完全的分析来确保可以进行直接调用,因而选择了更安全(也更慢)的虚拟方法调用。

假设我们知道农场中没有类型的牛了,因此Cow(牛)这个类不会产生衍生类。如果我们清楚告知编译器这点,就能获得一个更好的结果。现将类的定义改为如下:

[C#] 纯文本查看 复制代码public sealed class Cow : Animal { public override string Speak() { return Moo; } }

关键字sealed将告诉编辑器 Cow(牛)不会有衍生类(sealed也可以直接应用于 Speak方法)。现在 IL2CPP能确信进行直接调用了:

[C++] 纯文本查看 复制代码// Create a new cow //创建一个新的牛 Cow_t1312235562 * L_14 = (Cow_t1312235562 *)il2cpp_codegen_object_new(Cow_t1312235562_il2cpp_TypeInfo_var); Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL); V_4 = L_14; Cow_t1312235562 * L_16 = V_4; // Look ma, no virtual call! //看到了么,没有虚拟调用啦! String_t* L_17 = Cow_Speak_m1607867742(L_16, /*hidden argument*/NULL);

这里调用 Speak就不会再慢了,因为我们已经清楚的告诉编译器,并且有把握地允许编译器进行优化。

这种优化不会使您的游戏运行速度显著变快。但这对于后来的人阅读代码和编辑器本身来说,都是表达任何编程设想“代码化”的上佳实践。如果您想使用IL2CPP编译,强烈建议仔细阅读项目生成的C++代码,可能会有意想不到的收获!

下一篇我们将讨论为什么虚方法调用消耗高,以及怎样使其变得更快。

原文链接:http://blogs.unity3d.com/2016/07/26/il2cpp-optimizations-devirtualization/
感谢Unity官方翻译组成员“fubb”对本文翻译所做的贡献。
转载请注明来源:Unity官方中文社区 (forum.china.unity3d.com)。请勿私自更改任何版权说明信息。 Unity, IL2CPP, 编译器优化锐亚教育

锐亚教育 锐亚科技 unity unity教程