il2cpp_IL2CPP优化:去虚拟化

本文介绍了Unity的IL2CPP编译器如何通过去虚拟化优化代码性能,解释了虚拟方法调用与直接方法调用的性能差异,并通过一个动物类的示例展示了如何通过密封类来促使IL2CPP进行直接方法调用,从而提高执行效率。虽然这种优化可能不会显著提升游戏性能,但它强调了在代码中明确表达假设以帮助编译器优化的重要性。
摘要由CSDN通过智能技术生成

il2cpp

The scripting virtual machine team at Unity is always looking for ways to make your code run faster. This is the first post in a three part miniseries about a few micro-optimizations performed by the IL2CPP AOT compiler, and how you can take advantage of them. While nothing here will make code run two or three times as fast, these small optimizations can help in important parts of a game, and we hope they give you some insight into how your code is executing.

Unity的脚本虚拟机团队一直在寻找使您的代码运行更快的方法。 这是一个由三部分组成的微型系列文章中的第一篇,有关IL2CPP AOT编译器执行的一些微优化以及如何利用它们进行了微优化。 尽管这里没有什么可以使代码运行速度提高两倍或三倍,但是这些小的优化可以帮助游戏的重要部分,我们希望它们能使您对代码的执行方式有所了解。

Modern compilers are excellent at performing many optimizations to improve run time code performance. As developers, we can often help our compilers by making information we know about the code explicit to the compiler. Today we’ll explore one micro-optimization for IL2CPP in some detail, and see how it might improve the performance of your existing code.

现代编译器擅长执行许多优化以提高运行时代码性能。 作为开发人员,我们通常可以通过向编译器显示有关代码的信息来帮助编译器。 今天,我们将详细探讨IL2CPP的一种微优化,并观察它如何改善现有代码的性能。

去虚拟化 (Devirtualization)

There is no other way to say it, virtual method calls are always more expensive than direct method calls. We’ve been working on some performance improvements in the libil2cpp runtime library to cut back the overhead of virtual method calls (more on this in the next post), but they still require a runtime lookup of some sort. The compiler cannot know which method will be called at run time – or can it?

没有别的说法,虚拟方法调用总是比直接方法调用更昂贵。 我们一直在努力改善libil2cpp运行时库中的性能,以减少虚拟方法调用的开销(在下一篇文章中会对此进行更多介绍),但是它们仍然需要某种类型的运行时查找。 编译器无法知道在运行时将调用哪种方法,或者可以吗?

Devirtualization is a common compiler optimization tactic which changes a virtual method call into a direct method call. A compiler might apply this tactic when it can prove exactly which actual method will be called at compile time. Unfortunately, this fact can often be difficult to prove, as the compiler does not always see the entire code base. But when it is possible, it can make virtual method calls much faster.

去虚拟化是一种常见的编译器优化策略,它将虚拟方法调用更改为直接方法调用。 当编译器可以确切证明 在编译时将调用 哪个 实际 方法 时,可以应用此策略 。 不幸的是,由于编译器并不总是能够看到整个代码库,所以这一事实通常很难证明。 但是在可能的情况下,它可以使虚拟方法调用更快。

典范的例子 (The canonical example)

As a young developer, I learned about virtual methods with a rather contrived animal example. This code might be familiar to you as well:

作为一个年轻的开发人员,我通过一个非常人为的动物示例了解了虚拟方法。 您可能也熟悉以下代码:

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
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";
   }
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
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" ;
   }
}

Then in Unity (version 5.3.5) we can use these classes to make a small farm:

然后在Unity(版本5.3.5)中,我们可以使用以下类创建一个小型服务器场:

1

2
3
4
5
6
7
8
9
10
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());
   }
}

1

2
3
4
5
6
7
8
9
10
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 ( ) ) ;
   }
}

Here each call to Speak is a virtual method call. Let’s see if we can convince IL2CPP to devirtualize any of these method calls to improve their performance.

在这里,每个对 Speak的 调用 都是一个虚拟方法调用。 让我们看看是否可以说服IL2CPP对这些方法调用中的任何一个进行虚拟化以提高其性能。

生成的C ++代码还不错 (Generated C++ code isn’t too bad)

One of the features of IL2CPP I like is that it generates C++ code instead of assembly code. Sure, this code doesn’t look like C++ code you would write by hand, but it is much easier to understand than assembly. Let’s see the generated code for the body of that foreach loop:

我喜欢的IL2CPP的功能之一是它生成C ++代码而不是汇编代码。 当然,此代码看起来不像您将要手工编写的C ++代码,但是比汇编更容易理解。 我们来看一下 foreach 循环 主体的生成代码 :

1

2
3
4
5
6
7
8
9
10
11
// Set up a local variable to point to the animal array
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
V_1 = ((L_5)->GetAt(static_cast<il2cpp_array_size_t>(L_7)));
Animal_t3277885659 * L_9 = V_1;
// Call the Speak method
String_t* L_10 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Animal::Speak() */, L_9);

1

2
3
4
5
6
7
8
9
10
11
// Set up a local variable to point to the animal array
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
V_1 = ( ( L_5 ) -> GetAt ( static_cast < il2cpp_array_size_t > ( L_7 ) ) ) ;
Animal_t3277885659 * L_9 = V_1 ;
// Call the Speak method
String_t* L_10 = VirtFuncInvoker0 < String_t* > :: Invoke ( 4 /* System.String AssemblyCSharp.Animal::Speak() */ , L_9 ) ;

I’ve removed a bit of the generated code to simplify things. See that ugly call to Invoke? It is going to lookup the proper virtual method in the vtable and then call it. This vtable lookup will be slower than a direct function call, but that is understandable. The Animal could be a Cow or a Pig, or some other derived type.

为了简化起见,我删除了一些生成的代码。 看到那个叫 Invoke的 丑陋电话 吗? 它将在vtable中查找适当的虚拟方法,然后调用它。 此vtable查找将比直接函数调用慢,但这是可以理解的。 该 动物 可以是 牛 或 猪 ,或一些其他派生类型。

Let’s look at the generated code for the second call to Debug.LogFormat, which is more like a direct method call:

让我们看一下第二个 Debug.LogFormat 调用的生成代码 ,它更像是直接方法调用:

1

2
3
4
5
6
7
8
// 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;
// Call the Speak method
String_t* L_17 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Cow::Speak() */, L_16);

1

2
3
4
5
6
7
8
// 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 ;
// Call the Speak method
String_t* L_17 = VirtFuncInvoker0 < String_t* > :: Invoke ( 4 /* System.String AssemblyCSharp.Cow::Speak() */ , L_16 ) ;

Even in this case we are still making the virtual method call! IL2CPP is pretty conservative with optimizations, preferring to ensure correctness in most cases. Since it does not do enough whole-program analysis to be sure that this can be a direct call, it opts for the safer (and slower) virtual method call.

即使在这种情况下,我们仍然在进行虚拟方法调用! IL2CPP在优化方面非常保守,在大多数情况下都希望确保正确性。 由于它没有进行足够多的整个程序分析来确保可以直接调用它,因此它选择了更安全(且更慢)的虚拟方法调用。

Suppose we know that there are no other types of cows on our farm, so no type will ever derive from Cow. If we make this knowledge explicit to the compiler, we can get a better result. Let’s change the class to be defined like this:

假设我们知道我们的农场中没有其他类型的母牛,那么就不会有任何类型的母牛来自 Cow 。 如果我们将此知识明确告知编译器,我们将获得更好的结果。 让我们更改要定义的类,如下所示:

1

2
3
4
5
public sealed class Cow : Animal {
   public override string Speak() {
       return "Moo";
   }
}

1

2
3
4
5
public sealed class Cow : Animal {
   public override string Speak ( ) {
       return "Moo" ;
   }
}

The sealed keyword tells the compiler that no one can derive from Cow (sealed could also be used directly on the Speak method). Now IL2CPP will have the confidence to make a direct method call:

所述 密封 关键字告诉编译器,没有人可以从 牛 派生 ( 密封 也可在 朗读 方法 直接使用 )。 现在,IL2CPP将有信心进行直接方法调用:

1

2
3
4
5
6
7
8
// 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);

1

2
3
4
5
6
7
8
// 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 ) ;

The call to Speak here will not be unnecessarily slow, since we’ve been very explicit with the compiler and allowed it to optimize with confidence.

在 这里 对 Speak 的调用 不会不必要地变慢,因为我们对编译器非常明确,并且可以放心地对其进行优化。

This kind of optimization won’t make your game incredibly faster, but it is a good practice to express any assumptions you have about the code in the code, both for future human readers of that code and for compilers. If you are compiling with IL2CPP, I encourage you to peruse the generated C++ code in your project and see what else you might find!

这种优化不会使您的游戏快得令人难以置信,但是 对于未来 的代码 读者和编译器来说, 表达对代码 中的代码的 任何假设都是一个好习惯 。 如果您正在使用IL2CPP进行编译,我建议您仔细阅读项目中生成的C ++代码,看看还会发现什么!

Next time we’ll discuss why virtual method calls are expensive, and what we are doing to make them faster.

下次,我们将讨论为什么虚方法调用昂贵,以及我们将如何使它们更快。

翻译自: https://blogs.unity3d.com/2016/07/26/il2cpp-optimizations-devirtualization/

il2cpp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值