Amélioration des performances de recherche de l'inspecteur Burst

Je m'appelle Jonas Reholt et je suis étudiant au sein de l'équipe de Burst. Je prends le blog pour partager mon parcours d'optimisation qui a permis de rendre possibles les récents changements de performance de l'inspecteur Burst. La recherche de l'inspecteur Burst est désormais 13 fois plus rapide, ce qui permet aux développeurs de se concentrer plus rapidement sur le code qui leur importe lors de l'optimisation des projets.
Poursuivez votre lecture pour découvrir comment vous pouvez utiliser Unity Profiler pour rechercher les goulets d'étranglement de performance dans votre programme et comment les résoudre.
Le compilateur Unity Burst transforme votre code C# en code assembleur hautement optimisé. L'inspecteur Burst vous permet d'inspecter ce code assembleur directement dans l'éditeur Unity, de sorte que vous n'avez pas besoin d'utiliser des outils externes pour une simple inspection du code.
Lorsque vous ouvrez l'inspecteur Burst pour la première fois et que vous sélectionnez une tâche cible à afficher, une fenêtre similaire à l'image ci-dessous s'affiche.

Comme vous pouvez le constater, l'inspecteur Burst permet de mettre en évidence la syntaxe, d'afficher des flèches de flux de branches et bien d'autres choses encore.
L'inspecteur essaiera de faire défiler l'assemblage qui met en œuvre la fonction cible choisie, mais il est également utile de rechercher dans la vue de l'assemblage des instructions spécifiques, des commentaires, etc. Cela nous amène au sujet de ce billet de blog.
Pour effectuer la recherche, l'inspecteur doit rechercher le résultat de l'assemblage d'origine et transformer ces indices en positions dans la vue de l'inspecteur. La fonctionnalité de recherche originale suivait le modèle illustré ci-dessous et s'appuyait fortement sur l'implémentation de System.String.IndexOf(*).
while (assemblyCode.IndexOf(key, accIdx) >= 0) {
// ...
// Do logic for handling search hits
// ...
}
L'exécution de la recherche ci-dessus sur 135 582 lignes de code d'assemblage pour un résultat de recherche commun (21 769 résultats au total) a entraîné un temps d'exécution d'environ 12 secondes pour la première recherche, et d'environ 5 secondes pour les recherches suivantes. Ce n'est pas vraiment un temps d'attente souhaitable pour un événement GUI, nous devions donc faire quelque chose. L'exécution de la recherche à l'aide de l'outil Unity Profiler a révélé que 37,3 % du temps d'exécution était consacré à la fonction IndexOf(*), comme le montre le graphique ci-dessous.

Une optimisation judicieuse doit tenir compte de la dépendance à l'égard de cette fonction, soit en procédant à une implémentation personnalisée, soit en modifiant complètement l'algorithme. Quel que soit l'algorithme utilisé, il s'agit de parcourir l'ensemble de la chaîne. Il est donc nécessaire de procéder à une mise en œuvre personnalisée pour trouver les correspondances. Dans ces conditions, il a semblé judicieux de commencer l'optimisation en conservant l'algorithme original, mais en créant une fonction IndexOf personnalisée .
Les 3,34 secondes passées sur LongTextArea.GetFragNrFromBlockIdx() proviennent de la récupération du code d'assemblage non coloré . Il est utilisé pour effectuer la recherche. L'inspecteur Burst enregistre actuellement le code d'assemblage deux fois - une fois formaté pour le rendu, et une fois non formaté.
L'écriture d'une fonction personnalisée a également l'avantage de réduire le nombre d'appels, puisqu'il y a actuellement un appel pour chaque résultat de recherche, plus un.
Le code source de IndexOf(*) révèle de nombreux contrôles de sécurité nécessaires à une implémentation générale robuste. Toutefois, dans notre cas, nous pouvons supposer que la plupart de ces vérifications sont vraies. Pour essayer d'exploiter au maximum les performances, vous devrez créer une fonction de type C afin d'éviter des éléments tels que le contrôle des limites.
Vous pouvez écrire la fonction en suivant le pseudo-code ci-dessous, où IsKeyMatch(*) vérifie simplement si la clé correspond ou non.
List<int> Search(string assemblyCode, string key, int accIdx) {
var hits = new List<int>();
for (i = accIdx; i < assemblyCode.len - key.len; i++) {
if (IsKeyMatch(assemblyCode, key, i)) {
hits.add(i);
i += key.len-1;
}
}
return hits;
}
Toutefois, comme le langage C# est un langage géré, cette fonction de type C exige que vous épingliez les objets gérés utilisés afin que le garbage collector ne relocalise pas l'adresse mémoire. Voici le code de base :
unsafe {
fixed (char* source = assemblyCode) {
fixed (char* needle = key) {
CustomIndexOf(source, key)
}
}
}
La combinaison de ces éléments vous permet de séparer la boucle while d'origine en un seul appel à l'outil de recherche d'indices et à la logique de traitement des résultats de la recherche :
matches = FindAllMathces(text, key)
foreach match {
...
Do logic for handling search hits
...
}
Quels ont été les gains ? Si l'on reprend le petit exemple précédent, cette modification du code permet de multiplier par 6,6 la vitesse de l'appel initial et par 13,2 la vitesse des appels suivants (mesurée en termes d'ancien/nouveau). Le ralentissement de la recherche initiale s'explique par la surcharge liée au chargement de l'assemblage non formaté afin d'éviter de trouver des correspondances dans les chaînes de couleurs.

Grâce à ces améliorations, les recherches à forte charge avec un peu moins de 22 000 résultats prendront désormais environ 1,8 seconde pour la recherche initiale, et environ 0,4 seconde pour les recherches suivantes. Cela rend l'inspecteur Burst plus utilisable pour les grands ensembles, car il n'y a plus assez de temps pour préparer une tasse de thé pendant chaque recherche.
Vous pouvez profiter de cette amélioration des performances dès maintenant avec le paquetage Burst 1.8.7.
Vous voulez en savoir plus sur Burst ? Connectez-vous avec nous sur le forum de Burst. Ne manquez pas de consulter d'autres blogs techniques d'autres développeurs Unity dans le cadre de lasérie permanente Tech from the Trenches.