Optimisations IL2CPP : Dévirtualisation

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 26, 2016|5 Min
Optimisations IL2CPP : Dévirtualisation
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.

L’équipe de scripts de machines virtuelles d’ Unity est toujours à la recherche de moyens pour accélérer l’exécution de votre code. Il s'agit du premier article d'une mini-série en trois parties sur quelques micro-optimisations effectuées par le compilateur IL2CPP AOT et comment vous pouvez en tirer parti. Bien que rien ici ne permettra au code de s'exécuter deux ou trois fois plus vite, ces petites optimisations peuvent aider dans des parties importantes d'un jeu, et nous espérons qu'elles vous donneront un aperçu de la façon dont votre code s'exécute.

Dévirtualisation

Il n’y a pas d’autre façon de le dire, les appels de méthodes virtuelles sont toujours plus coûteux que les appels de méthodes directes. Nous avons travaillé sur certaines améliorations de performances dans la bibliothèque d'exécution libil2cpp pour réduire la surcharge des appels de méthodes virtuelles (plus d'informations à ce sujet dans le prochain article), mais elles nécessitent toujours une recherche d'exécution d'une certaine sorte. Le compilateur ne peut pas savoir quelle méthode sera appelée au moment de l'exécution - ou le peut-il ?

La dévirtualisation est une tactique d'optimisation courante du compilateur qui transforme un appel de méthode virtuelle en un appel de méthode direct. Un compilateur peut appliquer cette tactique lorsqu'il peut prouver exactement quelle méthode réelle sera appelée au moment de la compilation. Malheureusement, ce fait peut souvent être difficile à prouver, car le compilateur ne voit pas toujours l’intégralité de la base de code. Mais lorsque cela est possible, il peut rendre les appels de méthodes virtuelles beaucoup plus rapides.

L'exemple canonique

En tant que jeune développeur, j'ai appris les méthodes virtuelles avec un exemple animal plutôt artificiel. Ce code pourrait également vous être familier :

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

Ensuite, dans Unity (version 5.3.5), nous pouvons utiliser ces classes pour créer une petite ferme :

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

Ici, chaque appel à Speak est un appel de méthode virtuelle. Voyons si nous pouvons convaincre IL2CPP de dévirtualiser l’un de ces appels de méthode pour améliorer leurs performances.

Le code C++ généré n'est pas trop mal

L’une des fonctionnalités d’IL2CPP que j’aime est qu’il génère du code C++ au lieu du code assembleur. Bien sûr, ce code ne ressemble pas au code C++ que vous écririez à la main, mais il est beaucoup plus facile à comprendre que l'assembleur. Voyons le code généré pour le corps de cette boucle pour chaque :

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

J'ai supprimé un peu du code généré pour simplifier les choses. Vous voyez cet appel laid à Invoke ? Il va rechercher la méthode virtuelle appropriée dans la vtable, puis l'appeler. Cette recherche dans une table virtuelle sera plus lente qu'un appel de fonction direct, mais cela est compréhensible. L'animal pourrait être une vache ou un cochon, ou un autre type dérivé.

Regardons le code généré pour le deuxième appel à Debug.LogFormat, qui ressemble davantage à un appel de méthode direct :

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

Même dans ce cas, nous effectuons toujours l’appel de méthode virtuelle ! IL2CPP est assez conservateur avec les optimisations, préférant garantir l'exactitude dans la plupart des cas. Comme il n'effectue pas suffisamment d'analyse de l'ensemble du programme pour être sûr qu'il peut s'agir d'un appel direct, il opte pour l'appel de méthode virtuelle, plus sûr (et plus lent).

Supposons que nous sachions qu’il n’y a pas d’autres types de vaches dans notre ferme, donc aucun type ne dérivera jamais de Vache. Si nous rendons cette connaissance explicite au compilateur, nous pouvons obtenir un meilleur résultat. Modifions la classe pour qu'elle soit définie comme ceci :

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

Le mot clé sealed indique au compilateur que personne ne peut dériver de Cow (sealed pourrait également être utilisé directement sur la méthode Speak). IL2CPP aura désormais la confiance nécessaire pour effectuer un appel de méthode direct :

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

L'appel à Speak ici ne sera pas inutilement lent, puisque nous avons été très explicites avec le compilateur et lui avons permis d'optimiser en toute confiance.

Ce type d'optimisation ne rendra pas votre jeu incroyablement plus rapide, mais c'est une bonne pratique d'exprimer toutes les hypothèses que vous avez sur le code dans le code, à la fois pour les futurs lecteurs humains de ce code et pour les compilateurs. Si vous compilez avec IL2CPP, je vous encourage à parcourir le code C++ généré dans votre projet et à voir ce que vous pourriez trouver d’autre !

La prochaine fois, nous discuterons des raisons pour lesquelles les appels de méthodes virtuelles sont coûteux et de ce que nous faisons pour les rendre plus rapides.