在这篇文章中,我们将研究在il2cpp.exe生成的c++代码中如何调用托管代码中的方法。具体来说,我们将研究六种不同类型的方法调用
在每种情况下,我们将关注生成的c++代码在做什么,特别是这些指令的成本。
Setup
我将使用Unity 5.0.1p4版本。我将在Windows上运行编辑器,并为WebGL平台构建。我在构建时启用了“Development Player”选项,并将“Enable Exceptions”选项设置为“Full”。
我将使用一个脚本文件进行构建,该脚本文件从上一篇文章修改而来,这样我们就可以看到不同类型的方法调用。脚本从接口和类定义开始:
3 4 5 6 7 8 9 |
interface Interface { int MethodOnInterface(string question); }
class Important : Interface { public int Method(string question) { return 42; } public int MethodOnInterface(string question) { return 42; } public static int StaticMethod(string question) { return 42; } } |
然后我们有一个常量字段和一个委托类型,都在后面的代码中使用:
3 |
private const string question = "What is the answer to the ultimate question of life, the universe, and everything?";
private delegate int ImportantMethodDelegate(string question); |
最后,这些是我们感兴趣的方法(加上强制性的Start方法,这里没有内容)
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
private void CallDirectly() { var important = ImportantFactory(); important.Method(question); }
private void CallStaticMethodDirectly() { Important.StaticMethod(question); }
private void CallViaDelegate() { var important = ImportantFactory(); ImportantMethodDelegate indirect = important.Method; indirect(question); }
private void CallViaRuntimeDelegate() { var important = ImportantFactory(); var runtimeDelegate = Delegate.CreateDelegate(typeof (ImportantMethodDelegate), important, "Method"); runtimeDelegate.DynamicInvoke(question); }
private void CallViaInterface() { Interface importantViaInterface = new Important(); importantViaInterface.MethodOnInterface(question); }
private void CallViaReflection() { var important = ImportantFactory(); var methodInfo = typeof(Important).GetMethod("Method"); methodInfo.Invoke(important, new object[] {question}); }
private static Important ImportantFactory() { var important = new Important(); return important; }
void Start () {} |
有了这些定义,我们开始吧。回忆一下,生成的c++代码将位于项目的Temp\StagingArea\Data\il2cppOutput目录中(只要编辑器保持打开状态)。不要忘记在生成的代码上生成Ctags,以帮助导航。
Calling a method directly
调用方法最简单(也最快,我们将看到)的方法是直接调用它。下面是calldirect方法生成的代码:
3 4 5 |
Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo); V_0 = L_0; Important_t1 * L_1 = V_0; NullCheck(L_1); Important_Method_m1(L_1, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_Method_m1_MethodInfo); |
最后一行是实际的方法调用。注意,它没有做任何特殊的事情,只是调用在c++代码中定义的自由函数。回想一下之前关于生成代码的文章,il2cpp.exe将所有方法生成为c++自由函数,方法包含两个参数,一个是this指针,表示该对象,一个是方法的参数,和一个隐藏参数,最后面的那个是隐藏参数,方法调用以类名_方法名_m/t来调用。IL2CPP脚本后端对生成的代码不使用c++成员函数或虚拟函数。接下来,调用静态方法目录应该是类似的。下面是callstaticmethoddirect方法生成的代码:
Important_StaticMethod_m3(NULL /*static, unused*/, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_StaticMethod_m3_MethodInfo);
我们可以说调用静态方法的开销更少,因为我们不需要创建和初始化一个对象实例。但是,方法调用本身是完全相同的,是对一个c++自由函数的调用。这里唯一的区别是第一个参数总是传递一个NULL值。
由于对静态方法和实例方法的调用之间的差异是如此之小,我们将只在本文的其余部分关注实例方法,但是这些信息同样适用于静态方法。
Calling a method via a compile-time delegate
对于稍微有点奇怪的方法调用,比如通过委托进行的间接调用,会发生什么?我们首先来看一个我称之为编译时委托的东西,这意味着我们知道在编译时哪个方法会被调用在哪个对象实例上。此类型调用的代码在CallViaDelegate方法中。它看起来像这样在生成的代码:
// Get the object instance used to call the method.
Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
V_0 = L_0;
Important_t1 * L_1 = V_0;
// Create the delegate.
IntPtr_t L_2 = { &Important_Method_m1_MethodInfo };
ImportantMethodDelegate_t4 * L_3 = (ImportantMethodDelegate_t4 *)il2cpp_codegen_object_new (InitializedTypeInfo(&ImportantMethodDelegate_t4_il2cpp_TypeInfo));
ImportantMethodDelegate__ctor_m4(L_3, L_1, L_2, /*hidden argument*/&ImportantMethodDelegate__ctor_m4_MethodInfo);
V_1 = L_3;
ImportantMethodDelegate_t4 * L_4 = V_1;
// Call the method
NullCheck(L_4);
VirtFuncInvoker1< int32_t, String_t* >::Invoke(&ImportantMethodDelegate_Invoke_m5_MethodInfo, L_4, (String_t*) &_stringLiteral1);
注意,这里实际调用的方法不是生成的代码的一部分。方法VirtFuncInvoker1
这里的Invoke方法是这样的:
4 5 6 7 8 9 10 11 |
template struct VirtFuncInvoker1 { typedef R (*Func)(void*, T1, MethodInfo*);
static inline R Invoke (MethodInfo* method, void* obj, T1 p1) { VirtualInvokeData data = il2cpp::vm::Runtime::GetVirtualInvokeData (method, obj); return ((Func)data.methodInfo->method)(data.target, p1, data.methodInfo); } }; |
调用libil2cpp 中的方法GetVirtualInvokeData,会在基于托管代码生成的虚表结构中查找一个虚方法,然后调用该方法。
为什么我们不使用c++ 11 variadic templates 来实现这些VirtFuncInvokerN
的方法?这看起来像是需要variadic templates的情况,的确如此。但是,由il2cpp.exe生成的c++代码必须与一些还不支持所有c++ 11特性(包括variadic templates)的c++编译器一起工作。至少在这种情况下,我们不认为为c++ 11编译器生成代码值得付出额外的复杂性
但为什么这是一个虚方法调用呢?我们不是在c#代码中调用了一个实例方法吗?回想一下,我们是通过c#委托调用实例方法的。再次查看上面生成的代码。我们实际要调用的方法是通过MethodInfo*(方法元数据)的参数ImportantMethodDelegate_Invoke_m5_MethodInfo 传入的:。如果我们在生成的代码中搜索名为“ImportantMethodDelegate_Invoke_m5”的方法,我们会看到调用实际上是对ImportantMethodDelegate类型的托管调用方法的调用。这是一个虚拟方法,因此我们需要进行一个虚拟调用
通过对c#代码做一个看起来很简单的更改,我们现在已经从单个调用到c++自由函数,到多个函数调用,再加上查找表。通过委托调用方法的开销要比直接调用相同方法的开销大得多。也就是在c#中通过委托调用方法,在生成的il2cpp中调用花销大一些
注意,在查看委托方法调用的过程中,我们还看到了通过虚拟方法调用的工作方式。
Calling a method via an interface
在c#中也可以通过接口调用方法。这个调用是由il2cpp.exe实现的,类似于一个虚拟方法调用:
Important_t1 * L_0 = (Important_t1 *)il2cpp_codegen_object_new (InitializedTypeInfo(&Important_t1_il2cpp_TypeInfo));
Important__ctor_m0(L_0, /*hidden argument*/&Important__ctor_m0_MethodInfo);
V_0 = L_0;
Object_t * L_1 = V_0;
NullCheck(L_1);
InterfaceFuncInvoker1< int32_t, String_t* >::Invoke(&Interface_MethodOnInterface_m22_MethodInfo, L_1, (String_t*) &_stringLiteral1);
注意,这里实际的方法调用是通过InterfaceFuncInvoker1::Invoke函数完成的,该函数位于生成的interfaceinvoker .h文件中。与VirtFuncInvoker1类类似,InterfaceFuncInvoker1类通过libil2cpp中的il2cpp::vm::Runtime::GetInterfaceInvokeData函数在虚拟表中执行查找。
为什么在libil2cpp中接口方法调用需要使用与虚拟方法调用不同的API ?注意,对InterfaceFuncInvoker1::Invoke的调用不仅传递要调用的方法及其参数,还传递要调用该方法的接口(本例中为L_1)。每个类型的虚函数表都被存储,这样接口方法就被写入到一个特定的偏移量中。因此,il2cpp.exe需要提供接口,以便确定调用哪个方法。
最后,在IL2CPP中,调用虚拟方法和通过接口调用方法具有相同的开销。
Calling a method via a run-time delegate
使用委托的另一种方法是在运行时通过委托创建它。CreateDelegate方法。这种方法类似于编译时委托,不同之处在于它在运行时可以有更多的修改方式。我们为这种灵活性付出了额外的函数调用。下面是生成的代码:
// Get the object instance used to call the method.
Important_t1 * L_0 = HelloWorld_ImportantFactory_m15(NULL /*static, unused*/, /*hidden argument*/&HelloWorld_ImportantFactory_m15_MethodInfo);
V_0 = L_0;
// Create the delegate.
IL2CPP_RUNTIME_CLASS_INIT(InitializedTypeInfo(&Type_t_il2cpp_TypeInfo));
Type_t * L_1 = Type_GetTypeFromHandle_m19(NULL /*static, unused*/, LoadTypeToken(&ImportantMethodDelegate_t4_0_0_0), /*hidden argument*/&Type_GetTypeFromHandle_m19_MethodInfo);
Important_t1 * L_2 = V_0;
Delegate_t12 * L_3 = Delegate_CreateDelegate_m20(NULL /*static, unused*/, L_1, L_2, (String_t*) &_stringLiteral2, /*hidden argument*/&Delegate_CreateDelegate_m20_MethodInfo);
V_1 = L_3;
Delegate_t12 * L_4 = V_1;
// Call the method
ObjectU5BU5D_t9* L_5 = ((ObjectU5BU5D_t9*)SZArrayNew(ObjectU5BU5D_t9_il2cpp_TypeInfo_var, 1));
NullCheck(L_5);
IL2CPP_ARRAY_BOUNDS_CHECK(L_5, 0);
ArrayElementTypeCheck (L_5, (String_t*) &_stringLiteral1);
*((Object_t **)(Object_t **)SZArrayLdElema(L_5, 0)) = (Object_t *)(String_t*) &_stringLiteral1;
NullCheck(L_4);
Delegate_DynamicInvoke_m21(L_4, L_5, /*hidden argument*/&Delegate_DynamicInvoke_m21_MethodInfo);
与运行时委托的情况一样,我们需要花费一些时间为方法的参数创建数组。然后我们对MethodBase::Invoke (MethodBase_Invoke_m24函数)进行一个虚拟方法调用。在我们最终到达实际的方法调用之前,这个函数又会调用另一个虚函数!
Conclusion
特别地,如果可能的话,我们希望避免通过运行时委托和反射进行调用。与往常一样,关于性能改进的最佳建议是尽早并且经常使用分析工具进行度量。
下次我们将深入研究方法实现,看看如何共享泛型方法的实现,以最小化生成的代码和可执行文件的大小。