10000 appels Update()

Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Pour un développeur expérimenté, ce code est un peu étrange.
1. On ne sait pas exactement comment cette méthode est appelée.
2. L'ordre dans lequel ces méthodes sont appelées n'est pas clair si vous avez plusieurs objets dans une scène.
3. Ce style de code ne fonctionne pas avec Intellisense.
Non, Unity n'utilise pas System.Reflection pour trouver une méthode magique chaque fois qu'il doit en appeler une.
Au lieu de cela, la première fois qu'un MonoBehaviour d'un type donné est consulté, le script sous-jacent est inspecté via l'exécution de script (Mono ou IL2CPP) pour savoir s'il possède des méthodes magiques définies et ces informations sont mises en cache. Si un MonoBehaviour possède une méthode spécifique, il est ajouté à une liste appropriée, par exemple si un script a une méthode Update définie, il est ajouté à une liste de scripts qui doivent être mis à jour à chaque image.
Pendant le jeu, Unity parcourt simplement ces listes et exécute des méthodes à partir de celles-ci, aussi simple que cela. C'est pourquoi il n'a pas d'importance que votre méthode de mise à jour soit publique ou privée.
L'ordre est spécifié par les paramètres de l'ordre d'exécution du script (menu : (Edition > Paramètres du projet > Ordre d'exécution du script). Ce n'est peut-être pas la meilleure façon de définir manuellement l'ordre de 1 000 scripts, mais si vous souhaitez qu'un script soit exécuté après tous les autres, cette méthode est acceptable. Bien sûr, à l'avenir, nous souhaitons disposer d'un moyen plus pratique de spécifier l'ordre d'exécution, en utilisant un attribut dans le code par exemple.
Nous utilisons tous un IDE d'une certaine sorte pour éditer nos scripts C# dans Unity, la plupart d'entre eux n'aiment pas les méthodes magiques pour lesquelles ils ne peuvent pas comprendre où elles sont appelées, le cas échéant. Cela conduit à des avertissements et rend la navigation dans le code plus difficile.
Parfois, les développeurs ajoutent une classe abstraite étendant MonoBehaviour, l'appellent BaseMonoBehaviour ou similaire et font en sorte que chaque script de leur projet étende cette classe. Ils y ont intégré quelques fonctionnalités de base utiles ainsi qu'un tas de méthodes magiques virtuelles comme celles-ci :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Cette structure rend l'utilisation de MonoBehaviours dans votre code plus logique mais présente un petit défaut. Je parie que vous l'avez déjà compris...
Tous vos MonoBehaviours seront dans toutes les listes de mises à jour qu'Unity utilise en interne, toutes ces méthodes seront appelées à chaque image pour tous vos scripts, la plupart du temps ne faisant rien du tout !
On pourrait se demander pourquoi quelqu’un devrait se soucier d’une méthode vide ? Le problème est que ce sont des appels du monde C++ natif vers le monde C# géré, ils ont un coût. Voyons quel est ce coût.
Pour cet article, j'ai créé un petit exemple de projet qui est disponible sur Github. Il dispose de 2 scènes qui peuvent être modifiées en appuyant sur un appareil ou en appuyant sur n'importe quelle touche de l'éditeur :
(1) Dans la première scène, 10 000 MonoBehaviours sont créés avec ce code à l'intérieur :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
(2) Dans la deuxième scène, 10 000 autres MonoBehaviours sont créés, mais au lieu d'avoir une mise à jour, ils ont une méthode UpdateMe personnalisée qui est appelée par un script de gestion à chaque image comme suit :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Le projet de test a été exécuté sur 2 appareils iOS compilés en Mono et IL2CPP en mode non-développement dans la configuration Release. Le temps a été mesuré comme suit :
Configurer un chronomètre dans la première mise à jour appelée (configuré dans l'ordre d'exécution du script),
Arrêtez le chronomètre à LateUpdate,
Faites la moyenne des temps sur quelques minutes.
Version Unity : 5.2.2f1
Version iOS : 9.0

OUAH! C'est beaucoup ! Il doit y avoir quelque chose qui ne va pas avec le test !
En fait, j'ai juste oublié de définir l'optimisation des appels de script sur Rapide mais sans exceptions, mais maintenant nous pouvons voir quel impact ce paramètre particulier a sur les performances... pas que quiconque s'en soucie plus avec IL2CPP.

OK, c'est mieux. Passons à IL2CPP.

Ici, nous voyons deux choses :
1. Cette optimisation particulière a toujours du sens dans IL2CPP.
2. IL2CPP a encore une marge de progression et au moment où j'écris cet article, les équipes de script et d'IL2CPP travaillent dur pour augmenter les performances. Par exemple, la dernière branche de script contient des optimisations rendant l'exécution du test 35 % plus rapide.
Je vais vous expliquer ce que fait Unity sous le capot dans quelques instants. Mais maintenant, modifions notre code Manager pour le rendre 5 fois plus rapide !
Si vous n’avez pas lu cette excellente série d’articles sur les composants internes d’IL2CPP, vous devriez le faire juste après avoir fini de lire celui-ci !
Il s'avère que si vous vouliez parcourir une liste de 10 000 éléments à chaque image, vous feriez mieux d'utiliser un tableau au lieu d'une liste, car dans ce cas, le code C++ généré est plus simple et l'accès au tableau est tout simplement plus rapide.
Dans le test suivant, j'ai changé List<ManagedUpdateBehavior> en ManagedUpdateBehavior[].

Ça a l’air bien meilleur !
Mise à jour: J'ai exécuté le test avec un tableau sur Mono et j'ai obtenu 0,23 ms.
Nous avons compris que l’appel de fonctions de C++ vers C# n’est pas rapide, mais découvrons ce que fait réellement Unity lors de l’appel de mises à jour sur tous ces objets. Le moyen le plus simple de le faire est d’utiliser Time Profiler d’Apple Instruments.
Notez qu'il ne s'agit pas d'un Mono vs. Test IL2CPP — la plupart des choses décrites plus loin sont également vraies pour une version iOS Mono.
J'ai lancé le test sur iPhone 6 avec Time Profiler, enregistré quelques minutes de données et sélectionné un intervalle d'une minute à inspecter. Nous nous intéressons à tout à partir de cette ligne :
Type de bloc inconnu « codeBlock », veuillez spécifier un sérialiseur pour celui-ci dans la propriété « serializers.types »
Si vous n'avez jamais utilisé d'instruments auparavant, vous voyez sur la droite les fonctions triées par temps d'exécution et les autres fonctions qu'elles appellent. La colonne la plus à gauche est le temps CPU en ms et le % de ces fonctions et des fonctions qu'elles appellent combinées, la deuxième colonne de gauche est le temps d'auto-exécution de la fonction. Notez que comme le processeur n'a pas été entièrement utilisé par Unity au cours de cette expérience, nous constatons que 10 secondes de temps processeur sont consacrées à nos mises à jour dans un intervalle de 60 secondes. Nous nous intéressons évidemment aux fonctions qui prennent le plus de temps à exécuter.
J'ai utilisé mes compétences folles en Photoshop et j'ai codé en couleur quelques zones pour que vous puissiez mieux comprendre ce qui se passe.

Au milieu, vous voyez notre méthode Update ou comment IL2CPP l'appelle — UpdateBehavior_Update_m18. Mais avant d’y arriver, Unity fait beaucoup d’autres choses.
Unity passe en revue tous les comportements pour les mettre à jour. La classe d'itérateur spéciale, SafeIterator, garantit que rien ne se casse si quelqu'un décide de supprimer l'élément suivant de la liste. La simple itération sur tous les comportements enregistrés prend 1 517 ms sur un total de 9 979 ms.
Ensuite, Unity effectue une série de vérifications pour s’assurer qu’il appelle une méthode existante valide sur un GameObject actif qui a été initialisé et sa méthode Start appelée. Vous ne voulez pas que votre jeu plante si vous détruisez un GameObject pendant la mise à jour, n'est-ce pas ? Ces vérifications prennent 2188 ms supplémentaires sur un total de 9979 ms.
Unity crée une instance de ScriptingInvocationNoArgs (qui représente un appel du côté natif au côté géré) avec ScriptingArguments et ordonne à la machine virtuelle IL2CPP d'appeler la méthode (fonction scripting_method_invoke). Cette étape prend 2061 ms sur un total de 9979 ms.
La fonction scripting_method_invoke vérifie que les arguments passés sont valides (900 ms), puis appelle la méthode Runtime::Invoke de la machine virtuelle IL2CPP (1520 ms). Tout d’abord, Runtime::Invoke vérifie si une telle méthode existe (1018 ms). Ensuite, il appelle une fonction RuntimeInvoker générée pour la signature de la méthode (283 ms). Il appelle à son tour notre fonction de mise à jour qui, selon Time Profiler, prend 42 ms pour s'exécuter.
Et une jolie table colorée.

Utilisons maintenant Time Profiler avec le test du gestionnaire. Vous pouvez voir sur la capture d'écran qu'il existe les mêmes méthodes (certaines d'entre elles prennent moins de 1 ms au total, elles ne sont donc même pas affichées) mais la majeure partie du temps d'exécution va en fait à la fonction UpdateMe (ou comment IL2CPP l'appelle — ManagedUpdateBehavior_UpdateMe_m14). De plus, il y a une vérification nulle insérée par IL2CPP pour s'assurer que le tableau sur lequel nous parcourons n'est pas nul.
L'image suivante utilise les mêmes couleurs.

Alors, qu'en pensez-vous maintenant, faut-il se soucier d'un petit appel de méthode ?
Pour être honnête, ce test n’est pas complètement juste. Unity fait un excellent travail en vous protégeant, vous et votre jeu, des comportements involontaires et des plantages : Ce GameObject est-il actif ? N'a-t-il pas été détruit pendant cette boucle de mise à jour ? La méthode Update existe-t-elle sur l'objet ? Que faire avec un MonoBehaviour créé pendant cette boucle de mise à jour ? — mon script de gestion ne gère rien de tout cela, il parcourt simplement une liste d'objets à mettre à jour.
Dans le monde réel, le script du gestionnaire aurait probablement été plus compliqué et plus lent à exécuter. Mais dans ce cas, je suis le développeur : je sais ce que mon code est censé faire et j'architecture ma classe de gestionnaire en sachant quel comportement est possible et ce qui ne l'est pas dans mon jeu. Unity ne possède malheureusement pas une telle connaissance.
Bien sûr, tout dépend de votre projet, mais sur le terrain, il n'est pas rare de voir un jeu utiliser un grand nombre de GameObjects dans la scène, chacun exécutant une certaine logique à chaque image. Habituellement, il s'agit d'un petit morceau de code qui ne semble rien affecter, mais lorsque le nombre devient très important, la surcharge liée à l'appel de milliers de méthodes de mise à jour commence à être perceptible. À ce stade, il est peut-être déjà trop tard pour modifier l'architecture du jeu et refactoriser tous ces objets dans un modèle de gestionnaire.
Vous disposez désormais des données, pensez-y au début de votre prochain projet.
