Conseils généraux d'optimisation

Introduction

Dans un monde idéal, les ordinateurs fonctionneraient à une vitesse infinie, et la seule limite à ce que nous pourrions réaliser serait notre imagination. Dans le monde réel, cependant, il est trop 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 reconnaître que, surtout 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 au quel on s'intéresse
  • Utilisation du profiler de Godot
  • Utilisation de profileurs tiers externes
  • Utilisation de profileurs / débogueurs GPU
  • Vérification de la fréquence d'images (avec vsync 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, en particulier sur les mobiles et les ordinateurs de bureau, si vous visez les mobiles.

Limites

Les profileurs de CPU sont souvent la méthode la plus utilisée pour mesurer les performances, mais ils ne disent pas toujours tout.

  • 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 profiler, par exemple pour un téléphone portable
  • 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, alors utilisez toujours le profilage et le chronométrage pour guider vos efforts.

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

Principes

Donald Knuth :

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 programmeur / 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/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/graphiques 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é des processeurs, est que les processeurs modernes sont presque toujours limités par la largeur de bande de la 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 localisation des données et l'accès linéaire, plutôt que de sauter partout 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 donc refaire des tests et des profils, 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 (par exemple, au chargement de niveau, ou en chargeant des fichiers de données pré-calculés).

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.

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 9x, réduit de la durée globale d'une image par 5x et augmente de 5x 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.