[原创] IL2CPP优化(上):去虚拟化

发表于2016-08-18
评论0 2.9k浏览
  我们将推出三篇系列文章介绍IL2CPP AOT编译器的小优化,帮助大家更好的理解代码执行的过程。本文为系列第一篇。
  Unity的脚本虚拟机团队一直在寻找让代码更快运行的方式。本系列文章将为大家介绍IL2CPP AOT编译器的小优化,并教大家如何利用它们。尽管这些小优化并不能让您的代码运行速度提高2-3倍,但它们会在游戏中起到重要作用,我们希望这些优化能让您对代码的执行方式有更深入的理解。
  现代编译器都很擅于执行一些优化来提高代码的运行时性能。作为开发者,我们经常可以通过将已了解功用的代码,通过一定信息明确传达给编译器,进而帮助编译器提高效率。本文将详细讲解一个关于 IL2CPP的小优化,并看看它将如何提高您现有代码的运行效率。

1、去虚拟化
  众所周知,虚方法的调用通常比直接调用的消耗更大。我们一直致力于改善libil2cpp运行库的性能以降低虚方法调用的消耗(下篇文章将介绍更多该内容),但某些排序算法仍然需要一些运行时的查找。编译器无法知道哪些方法会在运行时被调用,亦或可不可以被调用?
  去虚拟化是一个很常见的编译器优化策略,也就是将虚方法调用改为直接的方法调用。当编译器编译时可以提供准确的“实际”方法时,这个策略就会被启用。不幸的是,这点往往很难做到,因为编译器通常无法看到整个代码库。然而,如果可以的话,这将使虚方法的调用更快速。

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















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) 中,我们可以使用这些类来做个小农场:










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去虚拟化这些方法调用是如何提高性能的。

3、生成的C++代码还不赖
  我比较喜欢IL2CPP的一个功能是它会生成C++代码而非汇编代码。当然,这些生成的代码与一般手写的看起来并不一样,但这种代码要比汇编代码容易理解得多。下面看看生成的 foreach循环体代码:














// 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(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,这个看起来更像是直接调用:










// 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(牛)这个类不会产生衍生类。如果我们清楚告知编译器这点,就能获得一个更好的结果。现将类的定义改为如下:





public sealed class Cow : Animal {
   public override string Speak() {
       return "Moo";
   }
}
  关键字sealed将告诉编辑器 Cow(牛)不会有衍生类(sealed也可以直接应用于 Speak方法)。现在 IL2CPP能确信进行直接调用了:










// 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)。请勿私自更改任何版权说明信息。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引