L2CPP Internals: Method calls

在这篇文章中,我们将研究在il2cpp.exe生成的c++代码中如何调用托管代码中的方法。具体来说,我们将研究六种不同类型的方法调用

  • Direct calls on instance and static methods:直接调用实例和静态方法
  • Calls via a compile-time delegate:通过编译时委托调用
  • Calls via a virtual method:通过虚方法调用
  • Calls via an interface method:通过接口调用
  • Calls via a run-time delegate:通过运行时委托调用
  • Calls via reflection:通过反射调用

在每种情况下,我们将关注生成的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<int32_t, String_t*>::Invoke位于generatedvirtualinvker .h文件中。该文件由il2cpp.exe生成,但它不来自任何IL代码。相反,il2cpp.exe根据返回值(VirtFuncInvokerN)和不返回值(VirtActionInvokerN)的虚函数的使用创建这个文件,其中N是方法的参数数。

这里的Invoke方法是这样的:

4

5

6

7

8

9

10

11

template <typename R, typename T1>

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

特别地,如果可能的话,我们希望避免通过运行时委托和反射进行调用。与往常一样,关于性能改进的最佳建议是尽早并且经常使用分析工具进行度量。

下次我们将深入研究方法实现,看看如何共享泛型方法的实现,以最小化生成的代码和可执行文件的大小。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TO_ZRG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值