il2cpp 热更_IL2CPP优化:更快的虚拟方法调用

il2cpp 热更_IL2CPP优化:更快的虚拟方法调用

il2cpp 热更

上次我们了解到虚拟方法调用比直接调用慢,并且我们发现了如何告诉IL2CPP可以将给定的虚拟方法调用转换(去虚拟化)为更快的直接方法调用。 但是,当您 必须 进行虚拟方法调用 时会发生什么 ? 让我们至少使其尽可能快。 (Last time we learned that virtual method calls are slower than direct calls, and we found out how to tell IL2CPP that a given virtual method call can be converted (devirtualized) into a faster direct method call. But what happens when you must make a virtual method call? Let’s at least make it as fast as possible.)

进行虚拟方法调用需要什么? (What does it take to make a virtual method call?)

A virtual method call is a call that must be resolved at run time. The compiler does not know which method will be called when it compiles the code, so it builds an array of methods (called the virtual table, or vtable) for each class. When someone calls one of those methods, the runtime looks up the proper method in the vtable, and calls it. But what happens when things don’t work out, and there is no virtual method to call in the vtable?

虚方法调用是必须在运行时解决的调用。 编译器不知道在编译代码时将调用哪个方法,因此它为每个类构建一个方法数组(称为虚拟表或vtable)。 当有人调用这些方法之一时,运行时将在vtable中查找适当的方法,然后对其进行调用。 但是,当事情无法解决并且在vtable中没有虚拟方法可以调用时,会发生什么?

当虚拟方法变坏时 (When virtual methods go bad)

Let’s look at an extreme example, where the object we use has a type created at run time:

让我们看一个极端的例子,其中我们使用的对象具有 在运行时创建 的 类型

1
2
3
4
5
6
7
8
9
10
11
class BaseClass {
   public virtual string SayHello() {
       return "Hello from base!";
   }
}
class GenericDerivedClass<T> : BaseClass {
   public override string SayHello() {
       return "Hello from derived!";
   }
}
1
2
3
4
5
6
7
8
9
10
11
class BaseClass {
   public virtual string SayHello ( ) {
       return "Hello from base!" ;
   }
}
class GenericDerivedClass < T > : BaseClass {
   public override string SayHello ( ) {
       return "Hello from derived!" ;
   }
}

Given these types, we can try this code in Unity (I’m using version 5.3.5):

给定这些类型,我们可以在Unity中尝试以下代码(我正在使用版本5.3.5):

1
2
3
4
5
6
7
8
9
10
public class VirtualInvokeExample : MonoBehaviour {
   void Start () {
       Debug.Log(MakeRuntimeBaseClass().SayHello());
   }
   private BaseClass MakeRuntimeBaseClass() {
       var derivedType = typeof(GenericDerivedClass<>).MakeGenericType(typeof(int));
       return (BaseClass)FormatterServices.GetUninitializedObject(derivedType);
   }
}
1
2
3
4
5
6
7
8
9
10
public class VirtualInvokeExample : MonoBehaviour {
   void Start ( ) {
       Debug . Log ( MakeRuntimeBaseClass ( ) . SayHello ( ) ) ;
   }
   private BaseClass MakeRuntimeBaseClass ( ) {
       var derivedType = typeof ( GenericDerivedClass <> ) . MakeGenericType ( typeof ( int ) ) ;
       return ( BaseClass ) FormatterServices . GetUninitializedObject ( derivedType ) ;
   }
}

The details of MakeRuntimeBaseClass are not too important. What really matters is the object it creates has a type (GenericDerivedClass<int>) which is created at run time.

MakeRuntimeBaseClass 的细节 不太重要。 真正重要的是它创建的对象具有 在运行时 创建的类型( GenericDerivedClass <int> )。

This somewhat odd code is no problem for a Just-in-time (JIT) compiler, where the compilation work happens at runtime. If we run it in the Unity editor, we get:

对于即时(JIT)编译器来说,这种有点奇怪的代码是没有问题的,在JIT编译器中,编译工作在运行时进行。 如果在Unity编辑器中运行它,则会得到:

1
2
3
4
Hello from derived!
UnityEngine.Debug:Log(Object)
VirtualInvokeExample:Start() (at Assets/VirtualInvokeExample.cs:7)
1
2
3
4
Hello from derived !
UnityEngine . Debug : Log ( Object )
VirtualInvokeExample : Start ( ) ( at Assets / VirtualInvokeExample . cs : 7 )

But the story is quite different with an Ahead-of-time (AOT) compiler. If we run this same code for iOS with IL2CPP, we get this exception:

但是使用Ahead-of-time(AOT)编译器的情况却大不相同。 如果我们使用IL2CPP在iOS上运行相同的代码,则会出现以下异常:

1
2
3
ExecutionEngineException: Attempting to call method 'GenericDerivedClass`1[[System.Int32, mscorlib, Version=2.0.5.0,
     Culture=, PublicKeyToken=7cec85d7bea7798e]]::SayHello' for which no ahead of time (AOT) code was generated.
 at VirtualInvokeExample.Start () [0x00000] in <filename unknown>:0
1
2
3
ExecutionEngineException : Attempting to call method 'GenericDerivedClass`1[[System.Int32, mscorlib, Version=2.0.5.0,
     Culture=, PublicKeyToken=7cec85d7bea7798e]]::SayHello' for which no ahead of time ( AOT ) code was generated .
 at VirtualInvokeExample . Start ( ) [ 0x00000 ] in < filename unknown > : 0

That type created at runtime (GenericDerivedType<int>) is causing problems for the SayHello virtual method call. Since IL2CPP is an AOT compiler, and there is no source code for the GenericDerivedType<int> type, IL2CPP did not generate an implementation of the SayHello method.

在运行时创建的类型( GenericDerivedType <int> )导致 SayHello 虚拟方法调用 出现问题 。 由于IL2CPP是AOT编译器,并且没有 GenericDerivedType <int> 类型的 源代码 ,因此IL2CPP不会生成 SayHello 方法 的实现 。

当您调用不存在的方法时 (When you call a method that does not exist)

To understand what is happening here, we can create an exception breakpoint in Xcode. That breakpoint is triggered inside the il2cpp::vm::Runtime::GetVirtualInvokeData function, where the libil2cpp runtime is attempting to resolve the virtual method to call. That function looks like this:

要了解这里发生的情况,我们可以 在Xcode中 创建一个 异常断点 。 该断点是在il2cpp :: vm :: Runtime :: GetVirtualInvokeData函数内部触发的,libil2cpp运行时在该函数试图解析要调用的虚拟方法。 该函数如下所示:

1
2
3
4
5
static inline void GetVirtualInvokeData(Il2CppMethodSlot slot, void* obj, VirtualInvokeData* invokeData) {
   *invokeData = ((Il2CppObject*)obj)->klass->vtable[slot];
   if (!invokeData->methodPtr)
       RaiseExecutionEngineException(invokeData->method);
}
1
2
3
4
5
static inline void GetVirtualInvokeData ( Il2CppMethodSlot slot , void * obj , VirtualInvokeData* invokeData ) {
   * invokeData = ( ( Il2CppObject* ) obj ) -> klass -> vtable [ slot ] ;
   if ( ! invokeData -> methodPtr )
       RaiseExecutionEngineException ( invokeData -> method ) ;
}

The first line does the lookup in the vtable that we discussed above. The second checks to see if the virtual method really exists, and throws the managed exception we saw if the method does not exist.

第一行在我们上面讨论的vtable中进行查找。 第二个方法检查虚拟方法是否确实存在,并在该方法不存在时抛出我们看到的托管异常。

让我们使这段代码更快 (Let’s make this code faster)

With only three lines of code here, can we make this any faster? As it turns out, we can! The vtable lookup is necessary, so that has to stay as-is. But what about that if check? Most of the time, the condition will be false (after all, look at the ugly code we needed to use to create a type at runtime and make the condition true). So why should we pay the cost of a branch in the code that we will seldom (or never) take?

这里仅用三行代码,我们可以使其更快吗? 事实证明,我们可以做到! vtable查找是必需的,因此必须保持原样。 但是,如果检查呢? 大多数情况下,条件将为假(毕竟,请看一下我们在运行时用于创建类型并使条件成立的丑陋代码)。 那么,为什么我们要用很少(或永远不会)采用的代码来支付分支的费用呢?

Instead, let’s always call a method! When that method is not generated by the AOT compiler, we’ll replace it with a method that throws a managed exception. In Unity 5.5 (currently in closed alpha release), GetVirtualInvokeData looks like this:

相反,我们 总是 调用一个方法! 当AOT编译器未生成该方法时,我们将其替换为引发托管异常的方法。 在Unity 5.5(当前处于封闭的Alpha版本)中,GetVirtualInvokeData如下所示:

1
2
3
4
static inline void GetVirtualInvokeData(Il2CppMethodSlot slot,
                    void* obj, VirtualInvokeData* invokeData) {
   *invokeData = ((Il2CppObject*)obj)->klass->vtable[slot];
}
1
2
3
4
static inline void GetVirtualInvokeData ( Il2CppMethodSlot slot ,
                    void * obj , VirtualInvokeData* invokeData ) {
   * invokeData = ( ( Il2CppObject* ) obj ) -> klass -> vtable [ slot ] ;
}

IL2CPP now generates a stub method for every different function signature used by any virtual method in the project. If a vtable slot doesn’t have a real method, it gets the proper stub method matching its function signature. In this case, the virtual method we call is:

现在,IL2CPP为 项目中任何虚拟方法使用的 每个不同 功能签名 生成一个存根方法 。 如果vtable插槽没有真正的方法,它将获得与其函数签名匹配的正确的stub方法。 在这种情况下,我们调用的虚拟方法是:

1
2
3
4
static  Il2CppObject * UnresolvedVirtualCall_2 (Il2CppObject * __this, const MethodInfo* method) {
    il2cpp_codegen_raise_execution_engine_exception(method);
    il2cpp_codegen_no_return();
}
1
2
3
4
static  Il2CppObject * UnresolvedVirtualCall_2 ( Il2CppObject * __this , const MethodInfo* method ) {
     il2cpp_codegen_raise_execution_engine_exception ( method ) ;
     il2cpp_codegen_no_return ( ) ;
}

So the code behaves in the same way, throwing a proper managed exception when the AOT compiler was not able to generate code for a virtual method call. Most importantly though, this behavior now has no cost for the normal case.

因此,代码的行为方式相同,当AOT编译器无法为虚拟方法调用生成代码时,将引发适当的托管异常。 不过,最重要的是, 对于正常情况 ,此行为现在 没有成本

这快多少? (How much faster is this?)

Now for the bottom line: Does this micro-optimization matter? Yes. Our profiling has shown between 3% – 4% improvement in overall execution time. The improvement varies depending on the number of virtual calls being made and processor architecture. Processors with better branch prediction pay a lower cost for the if check, so they see less benefit when it is removed. Processors that don’t handle branch prediction as well get a larger benefit in performance.

现在最重要的是: 这种微优化重要吗? 是的 。 我们的分析显示, 总体执行时间缩短了3%-4%。 改进取决于进行的虚拟调用数量和处理器体系结构。 具有更好分支预测的处理器为if支票支付的成本较低,因此,删除支票时,其收益将减少。 不处理分支预测的处理器也会在性能上获得更大的收益。

This is actually a common optimization technique for virtual machines, so we’re happy to be able to bring it to IL2CPP as well. It follows the old performance mantra, “executing no code is better than executing some code.”

这实际上是虚拟机的常见优化技术,因此我们很高兴也能够将其引入IL2CPP。 它遵循了旧的性能口号:“不执行代码比执行某些代码要好。”

Next time we’ll explore another micro-optimization, where IL2CPP can avoid executing code altogether if we can prove that it does not matter.

下次,我们将探索另一个微优化,如果可以证明这没关系,IL2CPP可以完全避免执行代码。

翻译自: https://blogs.unity3d.com/2016/08/04/il2cpp-optimizations-faster-virtual-method-calls/

il2cpp 热更