Fonctionnement de l'optimisation guidée par le profil (PGO)

L'optimisation guidée par le profil (également appelée PGO ou "pogo") permet d'affiner davantage les versions optimisées de votre jeu en exploitant des informations sur son comportement en situation réelle. De cette manière, le code peu fréquemment exécuté, tel que les erreurs ou les cas particuliers, est moins accentué par rapport aux chemins d'exécution critiques de votre code. Le processus est ainsi accéléré.

Schéma illustrant le fonctionnement de PGO

Figure 1 : Présentation du fonctionnement de PGO

Pour utiliser PGO, vous devez d'abord instrumenter votre build afin de générer des données de profil avec lesquelles le compilateur pourra travailler. Vous devez ensuite expérimenter le code en exécutant ce build et en générant un ou plusieurs fichiers de données de profil. Enfin, vous devez copier ces fichiers à partir de l'appareil et les utiliser avec le compilateur pour optimiser le fichier exécutable à l'aide des informations de profil que vous avez capturées.

Fonctionnement des builds optimisés sans PGO

Un build optimisé sans exploiter les données de profil s'appuie sur plusieurs méthodes heuristiques pour déterminer comment générer du code optimisé.

Certaines d'entre elles sont signalées de manière explicite par le développeur (par exemple, en C++ 20 ou version ultérieure), à l'aide d'indications de direction des branches telles que [[likely]] et [[unlikely]]. Un autre exemple consiste à utiliser le mot clé inline, voire __forceinline (bien qu'en général, il soit préférable et plus flexible de s'en tenir au premier). Par défaut, certains compilateurs supposent que la première section d'une branche (c'est-à-dire l'instruction if, et non la partie else) est la plus probable. L'optimiseur peut également émettre des hypothèses sur la façon dont le code s'exécutera à partir d'une analyse statique, mais sa portée est généralement limitée.

Le problème avec ces méthodes heuristiques est qu'elles ne peuvent pas aider le compilateur correctement en toutes circonstances, même avec un balisage manuel exhaustif. Par conséquent, même si le code généré est globalement bien optimisé, il n'est pas aussi performant qu'il pourrait l'être si le compilateur avait plus d'informations sur son comportement lors de l'exécution.

Générer un profil

Lorsque l'exécutable est créé et que la fonctionnalité PGO est activée en mode instrumenté, du code y est ajouté au début de chaque bloc de code (par exemple, au début d'une fonction ou de chaque ramification d'une branche). Ce code permet de suivre le nombre de fois où le bloc est saisi par l'exécution du code, ce que le compilateur pourra exploiter ultérieurement pour générer du code optimisé.

D'autres types de suivi sont également effectués, tels que la taille des opérations de copie habituelles dans un bloc afin de pouvoir générer des versions rapides et intégrées de l'opération ultérieurement.

Une fois qu'une tâche représentative a été réalisée par le jeu, l'exécutable doit appeler une fonction (__llvm_profile_write_file()) pour écrire les données de profil à un emplacement personnalisable de l'appareil. Cette fonction est associée automatiquement à votre jeu lorsque l'instrumentation PGO est activée sur votre configuration de compilation.

Le fichier de données de profil écrit doit ensuite être copié sur l'ordinateur hôte et, de préférence, stocké au même emplacement que celui des autres profils de la même compilation afin de pouvoir les utiliser ensemble.

Par exemple, vous pouvez modifier votre code de jeu pour appeler __llvm_profile_write_file() à la fin de la scène du jeu en cours. Ensuite, pour créer un profil, vous devez compiler votre jeu alors que l'instrumentation est activée, puis le déployer sur votre appareil Android. Pendant l'exécution du jeu, les données de profil sont capturées automatiquement. Votre ingénieur de contrôle qualité parcourt le jeu, en expérimentant différents scénarios (ou passe simplement son test normal).

Lorsque vous avez terminé d'expérimenter les différentes parties de votre jeu, vous pouvez revenir au menu principal, ce qui met fin à la scène actuelle du jeu et permet d'écrire les données de profil.

Un script peut ensuite être utilisé pour copier les données de profil de l'appareil de test, puis les importer dans un dépôt central où elles pourront être exploitées ultérieurement.

Fusionner les données de profil

Une fois le profil obtenu à partir d'un appareil, il doit être converti à partir du fichier de données de profil généré par le build instrumenté, dans un format que le compilateur pourra utiliser. AGDE effectue automatiquement cette opération pour tous les fichiers de données de profil que vous ajoutez à votre projet.

La fonctionnalité PGO est conçue pour combiner les résultats de plusieurs exécutions de profil instrumentées. AGDE réalise également cette opération automatiquement si vous avez plusieurs fichiers dans un même projet.

Pour illustrer l'utilité de la fusion d'ensembles de données de profil, supposons que vous ayez rassemblé plusieurs ingénieurs de contrôle qualité et qu'ils jouent tous à différents niveaux de votre jeu. Chacune de ces parties est enregistrée, puis utilisée pour générer des données de profil à partir d'une version de votre jeu instrumentée avec PGO. La fusion de profils vous permet de combiner les résultats de ces différents cycles de test (qui peuvent exécuter des parties complètement différentes de votre code) afin d'obtenir des résultats plus affinés.

Mieux encore, lorsque vous effectuez des tests longitudinaux, où vous conservez des copies des données de profil d'une version interne à une autre, la recompilation n'invalide pas nécessairement les anciennes données de profil. La plupart du temps, le code est relativement stable d'une version à l'autre. Par conséquent, les données de profil des anciennes versions peuvent toujours être utiles et ne deviennent pas obsolètes immédiatement.

Générer des builds optimisés guidés par le profil

Une fois les données de profil ajoutées à votre projet, vous pouvez les utiliser pour compiler le fichier exécutable en activant la fonctionnalité PGO en mode Optimisation dans votre configuration de compilation.

Cela indique à l'optimiseur du compilateur d'utiliser les données de profil que vous avez collectées précédemment lors de ses décisions d'optimisation.

Quand utiliser l'optimisation guidée par le profil ?

L'optimisation guidée par le profil, PGO, n'est pas un service que vous activez au début du développement ou lors des itérations quotidiennes du code. Au cours du développement, concentrez-vous sur les optimisations basées sur des algorithmes et sur la mise en page des données, car elles apportent des avantages bien plus importants.

PGO intervient plus tard au cours du processus de développement, lorsque vous peaufinez la publication. Considérez l'optimisation guidée par le profil comme la cerise sur le gâteau, car elle vous permet de repousser les limites de performances de votre code après avoir déjà passé du temps à l'optimiser vous-même.

Amélioration prévue des performances avec PGO

L'amélioration prévue des performances avec PGO dépend d'un grand nombre de facteurs, y compris du niveau d'exhaustivité et d'obsolescence de vos profils, ainsi que du niveau d'optimisation qu'aurait atteint votre code avec un build optimisé classique.

En général, une estimation très prudente est une réduction des coûts du processeur d'environ 5 % dans les principaux threads. Les résultats peuvent toutefois varier.

Frais d'instrumentation

L'instrumentation avec PGO est exhaustive, et bien qu'elle soit générée automatiquement, elle n'est pas sans frais. Les frais liés à l'instrumentation avec PGO peuvent varier en fonction de votre codebase.

Coût lié aux performances de l'instrumentation guidée par profil

Vous constaterez peut-être une baisse de la fréquence d'images avec les builds instrumentés. Dans certains cas, selon que votre utilisation du processeur est proche de 100 % en fonctionnement normal, cette baisse peut être tellement importante qu'elle rend la jouabilité difficile.

Nous recommandons à la plupart des développeurs de créer un mode de rejeu semi-déterministe. Ce type de fonctionnalité permet à l'équipe de contrôle qualité de lancer le jeu à un point de départ connu et reproductible (par exemple, lorsque la progression a été enregistrée ou qu'un niveau de test spécifique a été atteint), puis d'enregistrer son entrée. Cette entrée enregistrée à partir du build de test peut être ingérée dans un build instrumenté avec PGO et rejouée. Elle peut générer des données de profil réelles, quel que soit le temps nécessaire au traitement d'un frame, même si le jeu était si lent qu'il était impossible d'y jouer.

Ce type de fonctionnalité présente également d'autres avantages majeurs, tels que la multiplication des efforts des testeurs : un testeur peut enregistrer son entrée sur un appareil, laquelle peut ensuite être rejouée sur plusieurs types d'appareils différents à des fins de test de détection de fumée.

Un système de rejeu comme celui-ci peut présenter des avantages considérables sur Android, dont l'écosystème comporte un grand nombre de variantes d'appareils. Il peut aussi faire partie intégrante du système de compilation d'intégration continue, vous permettant ainsi d'effectuer régulièrement des tests de régression et des tests de détection de fumée pendant la nuit.

L'enregistrement doit capturer l'entrée utilisateur au point le plus approprié du mécanisme d'entrée de votre jeu (probablement pas les événements directs de l'écran tactile, mais plutôt leurs conséquences sous forme de commandes). Ces entrées doivent également comporter un nombre de frames qui augmente de manière monotone pendant le jeu, de sorte que lors de la lecture, le mécanisme de rejeu puisse attendre le frame approprié pour déclencher un événement.

En mode lecture, votre jeu doit éviter de nécessiter une connexion en ligne, ne doit pas afficher d'annonces et doit fonctionner à un rythme fixe (en fonction de votre fréquence d'images cible). Envisagez de désactiver vsync.

Il n'est pas important que tous les éléments (par exemple, les systèmes de particules) de votre jeu soient parfaitement reproductibles d'un point de vue déterministe. Cependant, les mêmes actions doivent avoir les mêmes conséquences et résultats dans le jeu. Autrement dit, les parties doivent être identiques.

Coût lié à la mémoire de l'instrumentation guidée par le profil

Le coût lié à la mémoire de l'instrumentation guidée par le profil varie beaucoup en fonction de la bibliothèque spécifique qui est compilée. D'après nos tests, la taille globale du fichier exécutable de test est multipliée par 2,2 environ. Cette augmentation de taille comprend à la fois le code supplémentaire nécessaire pour instrumenter les blocs de code et l'espace requis pour stocker les compteurs. Ces tests n'étaient pas exhaustifs. Il se peut donc que les résultats soient différents pour vous.

Quand mettre à jour ou supprimer vos données de profil ?

Vous devez mettre à jour vos profils chaque fois que vous apportez une modification importante au code (ou au contenu d'un jeu).

Plus précisément, cela dépend de votre environnement de compilation et du stade de développement que vous avez atteint.

Comme indiqué précédemment, vous ne devez pas transférer de données de profil lors de changements majeurs d'environnement de compilation. Bien que cette action n'empêche pas en soi de créer le build, pas plus qu'elle n'y nuit, les avantages en termes de performances liés à l'utilisation de PGO seraient moindres, car très peu de données de profil s'appliqueraient au nouvel environnement de compilation. Cependant, ce n'est pas le seul cas où les données de profil peuvent devenir obsolètes.

Supposons que vous n'utilisiez pas PGO avant la fin du développement lorsque vous préparez une version finale et que vous vous contentiez, par exemple, d'effectuer une capture hebdomadaire afin de permettre aux ingénieurs axés sur les performances de vérifier qu'il n'y aura pas de problème inattendu avant la sortie du jeu.

Cette situation change à l'approche de la sortie du jeu, lorsque l'équipe de contrôle qualité réalise des tests quotidiens et examine le jeu de manière exhaustive. Au cours de cette phase, vous pouvez générer des profils à partir de ces données quotidiennement, les utiliser comme référence pour les futurs builds et ajuster vos propres budgets de performances.

Lorsque vous vous préparez à la sortie d'un jeu, vous devez verrouiller la version que vous prévoyez de publier. L'équipe de contrôle qualité doit ensuite l'exécuter afin de générer vos nouvelles données de profil. Vous pouvez ainsi utiliser ces données comme référence pour créer une version finale de votre fichier exécutable.

L'équipe chargée du contrôle qualité peut ensuite passer une dernière fois au crible le build final optimisé afin de s'assurer qu'il est prêt à être publié.