Conseils généraux d'optimisation

Introduction

Dans un monde idéal, les ordinateurs fonctionneraient à une vitesse infinie. La seule limite à ce que nous pourrions réaliser serait notre imagination. Dans le monde réel, cependant, il est très facile de produire des logiciels qui mettront à genoux même l'ordinateur le plus rapide.

La conception de jeux et d'autres logiciels est donc un compromis entre ce que nous voudrions être possible et ce que nous pouvons réaliser de manière réaliste tout en maintenant de bonnes performances.

Pour obtenir les meilleurs résultats, nous avons deux approches :

  • Travailler plus vite.
  • Travailler plus intelligemment.

Et de préférence, nous utiliserons un mélange des deux.

Fumée et miroirs

Pour travailler plus intelligemment, il faut notamment reconnaître que, dans les jeux, nous pouvons souvent faire croire au joueur qu'il se trouve dans un monde bien plus complexe, interactif et graphiquement passionnant qu'il ne l'est en réalité. Un bon programmeur est un magicien, et doit s'efforcer d'apprendre les ficelles du métier, et essayer d'en inventer de nouvelles.

La nature de la lenteur

Pour l'observateur extérieur, les problèmes de performance sont souvent regroupés ensemble. Mais en réalité, il existe plusieurs types de problèmes de performance différents :

  • Un processus lent qui se produit à chaque image, conduisant à une fréquence d'images continuellement faible.
  • Un processus intermittent qui provoque des "pics" de lenteur, conduisant à des décrochages.
  • Un processus lent qui se produit en dehors du gameplay normal, par exemple, lors du chargement des niveaux.

Chacun d'entre eux est ennuyeux pour l'utilisateur, mais de manière différente.

Mesure des performances

L'outil le plus important pour l'optimisation est probablement la capacité à mesurer les performances - à identifier les goulets d'étranglement et à mesurer le succès de nos tentatives pour les accélérer.

Il existe plusieurs méthodes de mesure des performances, notamment :

  • Mise en place d'un timer de démarrage/arrêt autour d'un code auquel nous nous intéressons.
  • Utilisation du profiler de Godot.
  • Utilisation de profileurs CPU tiers externes.
  • Utiliser des profileurs/débogueurs GPU tels que NVIDIA Nsight Graphics ou apitrace.
  • Vérification de la fréquence d'images (avec V-Sync désactivé).

Soyez très conscient que les performances relatives des différents domaines peuvent varier sur différents matériels. Il est souvent judicieux de faire des chronométrages sur plusieurs appareils. C’est particulièrement le cas si vous ciblez les appareils mobiles.

Limites

Les profileurs de CPU sont souvent la méthode la plus utilisée pour mesurer les performances. Cependant, ils ne racontent pas toujours toute l’histoire.

  • Les goulots d'étranglement sont souvent au niveau du GPU, "en conséquence" d'instructions données par le CPU.
  • Des pics peuvent se produire dans les processus du système d'exploitation (en dehors de Godot) "en conséquence" d'instructions utilisées dans Godot (par exemple l'allocation dynamique de mémoire).
  • Il se peut que vous ne puissiez pas toujours profiler un appareil spécifique comme un téléphone portable en raison de la configuration initiale requise.
  • Vous devrez peut-être résoudre des problèmes de performance qui se produisent sur du matériel auquel vous n'avez pas accès.

En raison de ces limitations, vous devez souvent faire un travail de détective pour trouver où se trouvent les goulets d'étranglement.

Travail de détective

Le travail de détective est une compétence cruciale pour les développeurs (tant en termes de performances que de correction de bogues). Cela peut inclure la vérification d'hypothèses et la recherche binaire.

Tests d'hypothèses

Disons, par exemple, que vous croyez que les sprites ralentissent votre jeu. Vous pouvez tester cette hypothèse par exemple en :

  • Mesurant les performances lorsque vous ajoutez des sprites ou en retirez.

Cela peut conduire à une autre hypothèse : la taille du sprite détermine-t-elle la baisse de performance ?

  • Vous pouvez tester cela en gardant tout de la même manière, mais en modifiant la taille du sprite et en mesurant les performances.

Profileurs

Les profileurs vous permettent de chronométrer votre programme pendant son exécution. Les profileurs fournissent ensuite des résultats vous indiquant le pourcentage de temps passé dans les différentes fonctions et domaines, et la fréquence d'appel des fonctions.

Cela peut être très utile pour identifier les goulets d'étranglement et pour mesurer les résultats de vos améliorations. Parfois, les tentatives d'amélioration des performances peuvent se retourner contre vous et entraîner un ralentissement des performances. Utilisez toujours le profilage et le chronométrage pour guider vos efforts.

Pour plus d'informations sur l'utilisation du profileur intégré de Godot, voir Panneau de débogage.

Principes

Donald Knuth dit :

Les programmeurs perdent énormément de temps à penser ou à s'inquiéter de la vitesse des parties non critiques de leurs programmes, et ces tentatives d'efficacité ont en fait un fort impact négatif lorsque l'on considère le débogage et la maintenance. Nous devrions oublier les petites efficacités, disons environ 97% du temps : l'optimisation prématurée est la racine de tous les maux. Pourtant, nous ne devrions pas laisser passer les opportunités dans ces 3 % critiques.

Les messages sont très importants :

  • Le temps du développeur est limité. Au lieu d'essayer aveuglément d'accélérer tous les aspects d'un programme, nous devrions concentrer nos efforts sur les aspects qui comptent vraiment.
  • Les efforts d'optimisation aboutissent souvent à un code plus difficile à lire et à déboguer qu'un code non optimisé. Il est dans notre intérêt de limiter ces efforts aux domaines qui en bénéficieront réellement.

Ce n'est pas parce que nous pouvons optimiser un morceau de code particulier, que nous devons nécessairement le faire. Savoir quand, et quand ne pas optimiser est une grande compétence à développer.

Un aspect trompeur de la citation est que les gens ont tendance à se concentrer sur la sous-citation "l'optimisation prématurée est la racine de tout mal". Alors que l'optimisation prématurée est (par définition) indésirable, un logiciel performant est le résultat d'une conception performante.

Design Performant

Le danger d'encourager les gens à ignorer l'optimisation jusqu'à ce qu'elle soit nécessaire, c'est qu'on ignore commodément que le moment le plus important pour considérer la performance est au stade de la conception, avant même de toucher le clavier. Si la conception ou les algorithmes d'un programme sont inefficaces, il n'est pas nécessaire de peaufiner les détails par la suite pour qu'il fonctionne rapidement. Il peut s'exécuter plus vite, mais il ne s'exécutera jamais aussi vite qu'un programme conçu pour la performance.

Cela tend à être beaucoup plus important dans la programmation de jeux ou de graphismes que dans la programmation générale. Une conception performante, même sans optimisation de bas niveau, fonctionnera souvent beaucoup plus vite qu'une conception médiocre avec une optimisation de bas niveau.

Design incrémentale

Bien sûr, en pratique, à moins d'avoir des connaissances préalables, il est peu probable que vous trouviez le meilleur design du premier coup. Vous réaliserez donc souvent une série de versions d'un domaine de code particulier, chacune adoptant une approche différente du problème, jusqu'à ce que vous trouviez une solution satisfaisante. Il est important de ne pas passer trop de temps sur les détails à ce stade jusqu'à ce que vous ayez finalisé la conception globale. Sinon une grande partie de votre travail sera jetée.

Il est difficile de donner des directives générales pour une conception performante car cela dépend fortement du problème. Un point qui mérite d'être mentionné, cependant, du côté du CPU, est que les CPUs modernes sont presque toujours limités par la bande passante mémoire. Cela a conduit à une résurgence de la conception orientée données, qui consiste à concevoir des structures de données et des algorithmes pour la localité de mise en cache des données et l'accès linéaire, plutôt que de sauter dans la mémoire.

Le processus d'optimisation

En supposant que nous ayons une conception raisonnable, et en tirant les leçons de Knuth, notre première étape dans l'optimisation devrait être d'identifier les plus gros goulets d'étranglement - les fonctions les plus lentes, les fruits les plus bas.

Une fois que nous aurons réussi à améliorer la vitesse de la zone la plus lente, il se peut qu'elle ne soit plus le goulot d'étranglement. Nous devons tester/profiler de nouveau,et trouver le prochain goulot d'étranglement sur lequel nous concentrer.

Le processus est donc :

  1. Profiler / Identifier les goulets d'étranglement.
  2. Optimiser le goulot d'étranglement.
  3. Retour à l'étape 1.

Optimiser les goulets d'étranglement

Certains profileurs vous diront même quelle partie d'une fonction (accès aux données, calculs) ralentit les choses.

Comme pour la conception, vous devez d'abord concentrer vos efforts pour vous assurer que les algorithmes et les structures de données sont les meilleurs possibles. L'accès aux données doit être local (pour utiliser au mieux le cache du CPU), et il est souvent préférable d'utiliser un stockage compact des données (là encore, toujours profiler pour tester les résultats). Souvent, on pré-calcule à l'avance des calculs lourds. Cela peut se faire en effectuant le calcul lors du chargement d'un niveau, en chargeant un fichier contenant des données pré-calculées ou simplement en stockant les résultats de calculs complexes dans une constante de script et en lisant sa valeur.

Une fois que les algorithmes et les données sont bons, vous pouvez souvent apporter de petites modifications aux routines qui améliorent les performances, par exemple en déplaçant les calculs hors des boucles ou transformer des boucles for imbriquées en boucles non imbriquées. (Cela devrait être possible si vous connaissez à l'avance la largeur ou la hauteur d'un tableau en 2D.)

Testez toujours à nouveau votre chronométrage/vos goulots d'étranglement après chaque changement. Certains changements augmenteront la vitesse, d'autres peuvent avoir un effet négatif. Parfois, un petit effet positif sera compensé par les inconvénients d'un code plus complexe, et vous pouvez choisir de ne pas tenir compte de cette optimisation.

Annexe

Mathématiques de goulot d'étranglement

Le proverbe "une chaîne est aussi forte que son maillon le plus faible" s'applique directement à l'optimisation des performances. Si votre projet passe 90 % de son temps dans la fonction A, l'optimisation de A peut avoir un effet massif sur les performances.

A: 9 ms
Everything else: 1 ms
Total frame time: 10 ms
A: 1 ms
Everything else: 1ms
Total frame time: 2 ms

Ainsi, dans cet exemple, l'amélioration de ce goulot d'étranglement A par un facteur de 9×, réduit de la durée globale d'une image par 5× et augmente de 5× les images par seconde.

Si, toutefois, un autre élément ralentit et est un goulet d’étranglement dans votre projet, la même amélioration peut conduire à des gains moins spectaculaires :

A: 9 ms
Everything else: 50 ms
Total frame time: 59 ms
A: 1 ms
Everything else: 50 ms
Total frame time: 51 ms

Ainsi, dans cet exemple, même si nous avons énormément optimisé la fonctionnalité A, le gain réel en termes de fréquence d'images est assez faible.

Dans les jeux, les choses deviennent encore plus compliquées parce que le CPU et le GPU fonctionnent indépendamment l'un de l'autre. Votre temps total d'une image est déterminé par le plus lent des deux.

CPU: 9 ms
GPU: 50 ms
Total frame time: 50 ms
CPU: 1 ms
GPU: 50 ms
Total frame time: 50 ms

Dans cet exemple, nous avons de nouveau optimisé le CPU de manière considérable, mais le temps d'une image ne s'est pas amélioré, car nous sommes confrontés à un goulet d'étranglement au niveau du GPU.