Internes IL2CPP : Appels de méthodes

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jun 3, 2015|11 Min
Internes IL2CPP : Appels de méthodes
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.

Il s’agit du quatrième article de blog de la série IL2CPP Internals . Dans cet article, nous verrons comment il2cpp.exe génère du code C++ pour les appels de méthode dans le code managé. Plus précisément, nous étudierons six types différents d’appels de méthodes :

- Appels directs sur les méthodes d'instance et statiques

- Appels via un délégué au moment de la compilation

- Appels via une méthode virtuelle

- Appels via une méthode d'interface

- Appels via un délégué d'exécution

- Appels via réflexion

Dans chaque cas, nous nous concentrerons sur ce que fait le code C++ généré et, plus précisément, sur le coût de ces instructions.

Comme pour tous les articles de cette série, nous allons explorer du code susceptible d’être modifié et qui, en fait, est susceptible d’être modifié dans une version plus récente d’ Unity. Cependant, les concepts devraient rester les mêmes. Veuillez considérer tout ce qui est discuté dans cette série comme des détails de mise en œuvre. Nous aimons exposer et discuter des détails comme celui-ci lorsque cela est possible !

Installation

J'utiliserai la version 5.0.1p4 Unity . J'exécuterai l'éditeur sur Windows et créerai pour la plate-forme WebGL. Je construis avec l'option « Development Player » activée et l'option « Activer les exceptions » définie sur la valeur « Complet ».

Je vais construire avec un seul fichier de script, modifié à partir du dernier article afin que nous puissions voir les différents types d'appels de méthodes. Le script commence par une définition d'interface et de classe :

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; }
}

Ensuite, nous avons un champ constant et un type délégué, tous deux utilisés plus tard dans le code :

private const string question = "What is the answer to the ultimate question of life, the universe, and everything?";

private delegate int ImportantMethodDelegate(string question);

Enfin, voici les méthodes qui nous intéressent (plus la méthode Start obligatoire, qui n'a pas de contenu ici) :

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 () {}

Maintenant que tout cela est défini, commençons. Rappelons que le code C++ généré sera situé dans le répertoire Temp\StagingArea\Data\il2cppOutput du projet (tant que l'éditeur reste ouvert). Et n'oubliez pas de générer des Ctags sur le code généré, pour vous aider à le naviguer.

Appeler une méthode directement

La manière la plus simple (et la plus rapide, comme nous le verrons) d’appeler une méthode est de l’appeler directement. Voici le code généré pour la méthode CallDirectly :

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);

La dernière ligne est l'appel de méthode réel. Notez qu'il ne fait rien de spécial, il appelle simplement une fonction libre définie dans le code C++. Rappelez-vous du post précédent sur le code généré, selon lequel il2cpp.exe génère toutes les méthodes en tant que fonctions libres C++. Le backend de script IL2CPP n'utilise pas de fonctions membres C++ ni de fonctions virtuelles pour aucun code généré. Il s’ensuit donc que l’appel d’un répertoire de méthodes statiques devrait être similaire. Voici le code généré à partir de la méthode CallStaticMethodDirectly :

Important_StaticMethod_m3(NULL /*static, unused*/, (String_t*) &_stringLiteral1, /*hidden argument*/&Important_StaticMethod_m3_MethodInfo);

Nous pourrions dire qu'il y a moins de frais généraux liés à l'appel d'une méthode statique, puisque nous n'avons pas besoin de créer et d'initialiser une instance d'objet. Cependant, l'appel de méthode lui-même est exactement le même, un appel à une fonction libre C++. La seule différence ici est que le premier argument est toujours passé avec une valeur NULL.

Étant donné que la différence entre les appels aux méthodes statiques et aux méthodes d'instance est minime, nous nous concentrerons uniquement sur les méthodes d'instance pour le reste de cet article, mais les informations s'appliquent également aux méthodes statiques.

Appel d'une méthode via un délégué au moment de la compilation

Que se passe-t-il avec un appel de méthode légèrement plus exotique, comme un appel indirect via un délégué ? Nous allons d’abord examiner ce que j’appellerai un délégué au moment de la compilation, ce qui signifie que nous savons au moment de la compilation quelle méthode sera appelée sur quelle instance d’objet. Le code pour ce type d'appel se trouve dans la méthode CallViaDelegate. Cela ressemble à ceci dans le code généré :

// 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);

J'ai ajouté quelques commentaires pour indiquer les différentes parties du code généré.

Notez que la méthode réelle appelée ici ne fait pas partie du code généré. La méthode VirtFuncInvoker1<int32_t, String_t*>::Invoke se trouve dans le fichier GeneratedVirtualInvokers.h. Ce fichier est généré par il2cpp.exe, mais il ne provient d'aucun code IL. Au lieu de cela, il2cpp.exe crée ce fichier en fonction de l'utilisation de fonctions virtuelles qui renvoient une valeur (VirtFuncInvokerN) et de celles qui ne le font pas (VirtActionInvokerN), où N est le nombre d'arguments de la méthode.

La méthode Invoke ressemble ici à ceci :

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);
}
};

L'appel à libil2cpp GetVirtualInvokeData recherche une méthode virtuelle dans la structure vtable générée en fonction du code managé, puis appelle cette méthode.

Pourquoi n'utilisons-nous pasdes modèles variadiquesC++11pour implémenter cesVirtFuncInvokerNméthodes ? Cela ressemble à une situation nécessitant des modèles variadiques, et c'est effectivement le cas. Cependant, le code C++ généré par il2cpp.exe doit fonctionner avec certains compilateurs C++ qui ne prennent pas encore en charge toutes les fonctionnalités de C++ 11, y compris les modèles variadiques. Dans ce cas au moins, nous n'avons pas pensé que le fork du code généré pour les compilateurs C++11 valait la peine de supporter la complexité supplémentaire.

Mais pourquoi s’agit-il d’un appel de méthode virtuelle ? N'appelons-nous pas une méthode d'instance dans le code C# ? Rappelons que nous appelons la méthode d’instance via un délégué C#. Regardez à nouveau le code généré ci-dessus. La méthode réelle que nous allons appeler est transmise via l'argument MethodInfo* (métadonnées de la méthode) : ImportantMethodDelegate_Invoke_m5_MethodInfo. Si nous recherchons la méthode nommée « ImportantMethodDelegate_Invoke_m5 » dans le code généré, nous voyons que l'appel est en fait à la méthode Invoke gérée sur le type ImportantMethodDelegate. Il s’agit d’une méthode virtuelle, nous devons donc effectuer un appel virtuel. C'est cette fonction ImportantMethodDelegate_Invoke_m5 qui fera réellement l'appel à la méthode nommée Method dans le code C#.

Wow, c'était vraiment une bouchée. En apportant ce qui semble être une simple modification au code C#, nous sommes désormais passés d'un seul appel à une fonction libre C++ à plusieurs appels de fonction, plus une recherche dans une table. Appeler une méthode via un délégué est nettement plus coûteux que d’appeler directement la même méthode.

Notez qu’en examinant un appel de méthode déléguée, nous avons également vu comment fonctionne un appel via une méthode virtuelle.

Appeler une méthode via une interface

Il est également possible d'appeler une méthode en C# via une interface. Cet appel est implémenté par il2cpp.exe de manière similaire à un appel de méthode virtuelle :

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);

Notez que l'appel de méthode réel ici est effectué via la fonction InterfaceFuncInvoker1::Invoke, qui se trouve dans le fichier GeneratedInterfaceInvokers.h. Comme la classe VirtFuncInvoker1, la classe InterfaceFuncInvoker1 effectue une recherche dans une vtable via la fonction il2cpp::vm::Runtime::GetInterfaceInvokeData dans libil2cpp.

Pourquoi un appel de méthode d'interface doit-il utiliser une API différente dans libil2cpp à partir d'un appel de méthode virtuelle ? Notez que l'appel à InterfaceFuncInvoker1::Invoke transmet non seulement la méthode à appeler et ses arguments, mais également l'interface sur laquelle appeler cette méthode (L_1 dans ce cas). La table virtuelle de chaque type est stockée de sorte que les méthodes d'interface soient écrites à un décalage spécifique. Par conséquent, il2cpp.exe doit fournir l'interface afin de déterminer quelle méthode appeler.

L’essentiel ici est que l’appel d’une méthode virtuelle et l’appel d’une méthode via une interface ont effectivement la même surcharge dans IL2CPP.

Appel d'une méthode via un délégué d'exécution

Une autre façon d'utiliser un délégué est de le créer au moment de l'exécution via la méthode Delegate.CreateDelegate. Cette approche est similaire à un délégué au moment de la compilation, sauf qu’il peut être modifié au moment de l’exécution de quelques manières supplémentaires. Nous payons cette flexibilité avec un appel de fonction supplémentaire. Voici le code généré :

// 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);

Ce délégué nécessite une bonne quantité de code pour sa création et son initialisation. Mais l’appel de méthode lui-même entraîne également une surcharge encore plus importante. Nous devons d’abord créer un tableau pour contenir les arguments de la méthode, puis appeler la méthode DynamicInvoke sur l’instance Delegate. Si nous suivons cette méthode dans le code généré, nous pouvons voir qu'elle appelle la fonction VirtFuncInvoker1::Invoke, tout comme le fait le délégué au moment de la compilation. Ce délégué nécessite donc un appel de fonction supplémentaire par rapport au délégué au moment de la compilation, ainsi que deux recherches dans une table virtuelle, au lieu d'une seule.

Appeler une méthode via la réflexion

Le moyen le plus coûteux d’appeler une méthode est, sans surprise, via la réflexion. Regardons le code généré pour la méthode CallViaReflection :

// 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;

// Get the method metadata from the type via reflection.
IL2CPP_RUNTIME_CLASS_INIT(InitializedTypeInfo(&Type_t_il2cpp_TypeInfo));
Type_t * L_1 = Type_GetTypeFromHandle_m19(NULL /*static, unused*/, LoadTypeToken(&Important_t1_0_0_0), /*hidden argument*/&Type_GetTypeFromHandle_m19_MethodInfo);
NullCheck(L_1);
MethodInfo_t * L_2 = (MethodInfo_t *)VirtFuncInvoker1< MethodInfo_t *, String_t* >::Invoke(&Type_GetMethod_m23_MethodInfo, L_1, (String_t*) &_stringLiteral2);
V_1 = L_2;
MethodInfo_t * L_3 = V_1;

// Call the method.
Important_t1 * L_4 = V_0;
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_3);
VirtFuncInvoker2< Object_t *, Object_t *, ObjectU5BU5D_t9* >::Invoke(&MethodBase_Invoke_m24_MethodInfo, L_3, L_4, L_5);

Comme dans le cas du délégué d’exécution, nous devons passer un certain temps à créer un tableau pour les arguments de la méthode. Ensuite, nous effectuons un appel de méthode virtuelle à MethodBase::Invoke (la fonction MethodBase_Invoke_m24). Cette fonction invoque à son tour une autre fonction virtuelle, avant que nous arrivions finalement à l’appel de méthode réel !

Conclusion

Bien que cela ne remplace pas le profilage et la mesure réels, nous pouvons avoir un aperçu de la surcharge de toute invocation de méthode donnée en examinant comment le code C++ généré est utilisé pour différents types d'appels de méthode. Plus précisément, il est clair que nous souhaitons éviter les appels via les délégués d’exécution et la réflexion, si possible. Comme toujours, le meilleur conseil pour améliorer les performances est de mesurer tôt et souvent avec des outils de profilage.

Nous recherchons toujours des moyens d'optimiser le code généré par il2cpp.exe, il est donc probable que ces appels de méthode seront différents dans une version ultérieure d' Unity.

La prochaine fois, nous approfondirons les implémentations de méthodes et verrons comment nous partageons l'implémentation de méthodes génériques pour minimiser le code généré et la taille de l'exécutable.