En général, à quelle fréquence et quand dois-je optimiser mon code?

13

Dans la programmation commerciale «normale», l'étape d'optimisation est souvent laissée jusqu'à ce qu'elle soit vraiment nécessaire. Ce qui signifie que vous ne devez pas optimiser jusqu'à ce qu'il soit vraiment nécessaire.

Rappelez-vous ce que Donald Knuth a dit "Nous devons oublier les petites efficacités, disons environ 97% du temps: l'optimisation prématurée est la racine de tout mal"

Quel est le moment d'optimiser pour m'assurer que je ne perds pas d'effort. Dois-je le faire au niveau de la méthode? Niveau de classe? Niveau module?

Quelle devrait également être ma mesure d'optimisation? Des tiques? Fréquence d'images? Temps total?

David Basarab
la source

Réponses:

18

Là où j'ai travaillé, nous utilisons toujours plusieurs niveaux de profilage; si vous voyez un problème, descendez un peu plus dans la liste jusqu'à ce que vous trouviez ce qui se passe:

  • Le "profileur humain", alias juste jouer le jeu ; cela semble-t-il lent ou "attelé" à l'occasion? Vous remarquez des animations saccadées? (En tant que développeur, notez que vous serez plus sensible à certains types de problèmes de performances et inconscient des autres. Planifiez des tests supplémentaires en conséquence.)
  • Activez l' affichage FPS , qui est un FPS moyen à fenêtre coulissante de 5 secondes. Très peu de frais généraux pour calculer et afficher.
  • Activez les barres de profil , qui ne sont qu'une série de quads (couleurs ROYGBIV) qui représentent différentes parties du cadre (par exemple, vblank, preframe, update, collision, render, postframe) en utilisant un simple chronomètre "chronomètre" autour de chaque section de code . Pour souligner ce que nous voulons, nous avons défini une largeur d'écran d'une barre représentative d'une image cible à 60 Hz, il est donc très facile de voir si vous êtes, par exemple, 50% sous le budget (seulement une demi-barre) ou 50% au-dessus ( la barre s'enroule et devient une barre et demie). Il est également assez facile de dire ce qui mange généralement la plupart du cadre: rouge = rendu, jaune = mise à jour, etc.
  • Construisez une construction instrumentée spéciale qui insère un "chronomètre" comme du code autour de chaque fonction. (Notez que vous pouvez prendre un énorme coup de performances, dcache et icache lorsque vous faites cela, donc c'est définitivement intrusif. Mais si vous manquez d'un profileur d'échantillonnage approprié ou d'un support décent sur le CPU, c'est une option acceptable. Vous pouvez également être intelligent. à propos de l'enregistrement d'un minimum de données sur la fonction entrée / sortie et la reconstruction des traces d'appels plus tard.) Lorsque nous avons construit les nôtres, nous avons imité une grande partie du format de sortie de gprof .
  • Mieux encore, exécutez un profileur d'échantillonnage ; VTune et CodeAnalyst sont disponibles pour x86 et x64, vous disposez de divers environnements de simulation ou d'émulation qui pourraient vous fournir des données ici.

(Il y a une histoire amusante d'un GDC d'un an d'un programmeur graphique qui a pris quatre photos de lui-même - heureux, indifférent, ennuyé et en colère - et a affiché une image appropriée dans le coin des constructions internes basée sur le framerate. les créateurs de contenu ont rapidement appris à ne pas activer les shaders compliqués pour tous leurs objets et environnements: ils mettraient le programmeur en colère. Contemplez le pouvoir du feedback.)

Notez que vous pouvez également faire des choses amusantes comme représenter graphiquement les "barres de profil" en continu, de sorte que vous pouvez voir les modèles de pointes ("nous perdons une image toutes les 7 images") ou similaires.

Pour répondre à votre question directement, cependant: d'après mon expérience, bien qu'il soit tentant (et souvent gratifiant - j'apprends généralement quelque chose) de réécrire des fonctions / modules uniques pour optimiser le nombre d'instructions ou les performances icache ou dcache, et nous devons réellement le faire parfois, lorsque nous avons un problème de performance particulièrement désagréable, la grande majorité des problèmes de performance que nous traitons régulièrement se résument à la conception . Par exemple:

  • Doit-on mettre en cache dans la RAM ou recharger à partir du disque les images d'animation d'état "d'attaque" pour le joueur? Et pour chaque ennemi? Nous n'avons pas de RAM pour tout faire, mais les charges de disque sont chères! Vous pouvez voir l'attelage si 5 ou 6 ennemis différents apparaissent en même temps! (D'accord, que diriez-vous d'un frai étonnant?)
  • Faisons-nous un seul type d'opération sur toutes les particules, ou toutes les opérations sur une seule particule? (Il s'agit d'un compromis icache / dcache, et la réponse n'est pas toujours claire.) Que diriez-vous de séparer toutes les particules et de stocker les positions ensemble (la fameuse "structure des tableaux") vs de conserver toutes les données de particules en un seul endroit (" tableau de structures ").

Vous l'entendez jusqu'à ce qu'il devienne désagréable dans les cours d'informatique de niveau universitaire, mais: il s'agit vraiment de structures de données et d'algorithmes. Passer du temps sur la conception d'algorithmes et de flux de données vous permettra d'en avoir plus pour votre argent en général. (Assurez-vous que vous avez lu les excellents pièges de la programmation orientée objet d'un collègue de Sony Developer Services pour un aperçu ici.) Cela ne "ressemble" pas à l'optimisation; c'est surtout du temps passé avec un tableau blanc ou un outil UML ou à créer de nombreux prototypes, plutôt que d'accélérer l'exécution du code actuel. Mais cela vaut généralement beaucoup plus la peine.

Et une autre heuristique utile: si vous êtes proche du «cœur» de votre moteur, cela peut valoir un effort supplémentaire et une expérimentation à optimiser (par exemple vectoriser ces multiplications de matrice!). Plus vous vous éloignez du cœur, moins vous devriez vous en préoccuper, à moins que l'un de vos outils de profilage ne vous dise le contraire.

maigre
la source
6
  1. Utilisez les bonnes structures de données et algorithmes dès le départ.
  2. Ne micro-optimisez pas jusqu'à ce que vous profiliez et sachiez exactement où se trouvent vos points chauds.
  3. Ne vous inquiétez pas d'être intelligent. Le compilateur fait déjà tous les petits trucs auxquels vous pensez ("Oh! J'ai besoin de multiplier par quatre! Je vais en déplacer deux à gauche!")
  4. Faites attention aux échecs de cache.
munificent
la source
1
S'appuyer sur le compilateur n'est intelligent qu'à un certain point. Oui, il fera des optimisations de judas auxquelles vous ne penseriez pas (et ne pourrait pas se passer d'assemblage), mais il ne sait pas ce que votre algorithme est censé faire, il ne peut donc pas faire d'optimisations intelligentes. En outre, vous seriez surpris du nombre de cycles que vous pouvez gagner en implémentant du code critique dans l'assembly ou les intrinsèques ... si vous savez ce que vous faites. Les compilateurs ne sont pas aussi intelligents qu'ils le prétendent, ils ne savent pas ce que vous faites à moins que vous ne le disiez explicitement partout (comme utiliser religieusement).
Kaj
1
Et encore une fois, je dois dire que si vous ne cherchez que des points chauds, vous manquerez beaucoup de cycles parce que vous ne trouverez aucun cycle ruisselant à travers le tableau (par exemple, les smartpointers ... des déréférences n'importe où, jamais en tant que hotspot car votre programme est en fait un hotspot).
Kaj
1
Je suis d'accord avec vos deux points, mais je regrouperais la majeure partie de cela sous "utiliser les bonnes structures de données et les bons algorithmes". Si vous faites circuler des pointeurs intelligents comptés par référence partout et que vous effectuez des cycles de saignement lors du comptage, vous avez certainement choisi la mauvaise structure de données.
munificent
5

Rappelez-vous cependant aussi "pessimisation prématurée". Bien qu'il ne soit pas nécessaire d'aller hardcore sur chaque ligne de code, il est justifié de réaliser que vous travaillez réellement sur un jeu, ce qui a des implications en temps réel sur les performances.
Alors que tout le monde vous dit de mesurer et d'optimiser les points chauds, cette technique ne vous montrera pas les performances perdues dans des endroits cachés. Par exemple, si chaque opération '+' de votre code prend deux fois plus de temps qu'elle ne le devrait, elle n'apparaîtra pas comme un point chaud et vous ne pourrez donc jamais l'optimiser ni même vous en rendre compte, cependant, étant donné qu'elle est utilisée partout dans le place, il pourrait vous coûter beaucoup de performances. Vous seriez surpris du nombre de ces cycles qui s'écoulent sans jamais être détectés. Soyez donc conscient de ce que vous faites.
En dehors de cela, j'ai tendance à profiler régulièrement pour avoir une idée de ce qui s'y trouve et du temps restant par image. Pour moi, le temps par image est le plus logique car il me dit directement où j'en suis avec les objectifs de fréquence d'images. Essayez également de savoir où se trouvent les pics et quelles en sont les causes - je préfère un taux de rafraîchissement stable à un taux de rafraîchissement élevé avec des pointes.

Kaj
la source
Cela me semble si mal. Bien sûr, mon «+» peut prendre deux fois plus de temps chaque fois qu'il est appelé, mais cela n'a vraiment d'importance que dans une boucle serrée. À l'intérieur d'une boucle serrée, changer un seul «+» peut faire des ordres de grandeur plus que changer un «+» en dehors de la boucle. Pourquoi penser à un dixième de microseconde, quand une milliseconde peut être économisée?
Wilduck
1
Ensuite, vous ne comprenez pas l'idée derrière la perte de ruissellement. '+' (à titre d'exemple) est appelé des centaines de milliers de fois par image, pas seulement en boucles serrées. Si cela perd quelques cycles à chaque fois que vous en perdez beaucoup, mais cela n'apparaîtra jamais comme un hotspot car les appels sont répartis uniformément sur votre base de code / chemin d'exécution. Vous ne parlez donc pas d'un dixième de microseconde, mais bien de milliers de fois ces dixièmes de microsecondes, ce qui représente plusieurs millisecondes. Après être allé chercher les fruits bas (boucles serrées), j'ai gagné plusieurs millisecondes de cette façon plus d'une fois.
Kaj
C'est comme un robinet qui coule. Pourquoi s'inquiéter de sauver cette petite goutte? - "Si votre robinet dégouline à raison d'une goutte par seconde, vous pouvez vous attendre à perdre 2700 gallons par an".
Kaj
Oh, je suppose qu'il n'était pas clair que je voulais dire quand l'opérateur + était surchargé, donc cela affecterait chaque '+' dans le code - vous ne voudriez en effet pas optimiser chaque '+' dans le code. Mauvais exemple, je suppose ... Je le pensais comme un exemple pour `` une fonctionnalité de base qui est appelée partout où l'implémentation pourrait être plus lente que prévu, en particulier lorsqu'elle est cachée par une surcharge d'opérateur ou d'autres constructions C ++ obscurcissantes ''.
Kaj
3

Une fois qu'un jeu est prêt à être publié (final ou bêta), ou qu'il est sensiblement lent, c'est probablement le meilleur moment pour profiler votre application. Bien sûr, vous pouvez toujours exécuter le profileur à tout moment; mais oui, l'optimisation prématurée est la racine de tout mal. Optimisation non fondée également; vous avez besoin de données réelles pour montrer qu'un certain code est lent, avant de tenter de "l'optimiser". Un profileur fait cela pour vous.

Si vous ne connaissez pas un profileur, apprenez-le! Voici un bon article de blog démontrant l'utilité d'un profileur.

La plupart de l'optimisation du code de jeu se résume à réduire les cycles de processeur dont vous avez besoin pour chaque image. Une façon de procéder consiste à optimiser chaque routine au fur et à mesure que vous l'écrivez et à vous assurer qu'elle est aussi rapide que possible. Cependant, il est communément admis que 90% des cycles du processeur sont dépensés dans 10% du code. Cela signifie que diriger tous vos travaux d'optimisation vers ces routines goulot d'étranglement aura 10 fois plus d'effet pour optimiser tout uniformément. Alors, comment identifiez-vous ces routines? Le profilage facilite les choses.

Sinon, si votre petit jeu tourne à 200 FPS même s'il contient un algorithme inefficace, avez-vous vraiment une raison d'optimiser? Vous devriez avoir une bonne idée des spécifications de votre machine cible et vous assurer que le jeu fonctionne bien sur cette machine, mais tout ce qui est au-delà de cela est (sans doute) du temps perdu qui pourrait être mieux utilisé pour coder ou polir le jeu.

Ricket
la source
Alors que les fruits bas ont en effet tendance à être dans 10% du code, et sont facilement capturés par le profilage à la fin, le fait de travailler uniquement par profilage pour cela vous fera manquer les routines qui sont souvent appelées mais qui ont juste un peu un peu de mauvais code chacun - ils n'apparaîtront pas dans votre profil mais ils saignent beaucoup de cycles par appel. Ça s'additionne vraiment.
Kaj
@Kaj, les bons profileurs résument toutes les centaines d'exécutions individuelles du mauvais algorithme et vous montrent le total. Ensuite, vous direz: "Et si vous aviez 10 mauvaises méthodes et que vous appeliez tous au 1 / 10e de la fréquence?" Si vous passez tout votre temps sur ces 10 méthodes, il vous manquera tous les fruits bas où vous en aurez pour votre argent beaucoup plus.
John McDonald,
2

Je trouve utile d'intégrer le profilage. Même si vous n'optimisez pas activement, il est bon d'avoir une idée de ce qui limite vos performances à un moment donné. De nombreux jeux ont une sorte de HUD superposable qui affiche un graphique simple (généralement juste une barre colorée) montrant combien de temps les différentes parties de la boucle de jeu prennent chaque image.

Ce serait une mauvaise idée de laisser l'analyse et l'optimisation des performances trop tardivement. Si vous avez déjà construit le jeu et que votre budget CPU est supérieur de 200% et que vous ne pouvez pas le trouver grâce à l'optimisation, vous êtes foutu.

Vous devez savoir quels sont les budgets pour les graphiques, la physique, etc., au moment où vous écrivez. Vous ne pouvez pas faire cela si vous n'avez aucune idée de ce que seront vos performances, et vous ne pouvez pas le deviner sans savoir à la fois quelles sont vos performances et combien il pourrait y avoir de mou.

Intégrez donc des statistiques de performance dès le premier jour.

Quant au moment de s'attaquer aux choses - encore une fois, il vaut probablement mieux ne pas le laisser trop tard, de peur de devoir refactoriser la moitié de votre moteur. D'un autre côté, ne soyez pas trop absorbé par l'optimisation des choses pour évincer chaque cycle si vous pensez que vous pourriez changer l'algorithme entièrement demain, ou si vous n'avez pas mis de vraies données de jeu à travers.

Cueillez les fruits bas pendant que vous avancez, abordez les gros trucs périodiquement, et ça devrait aller.

JasonD
la source
Pour ajouter au profileur de jeu (avec lequel je suis totalement d'accord), l'extension de votre profileur de jeu pour afficher plusieurs barres (pour plusieurs images) vous aide à corréler le comportement du jeu aux pointes et peut vous aider à trouver des goulots d'étranglement qui n'apparaîtront pas dans votre capture moyenne avec un profileur.
Kaj
2

Si l'on regarde la citation de Knuth dans son contexte, il continue d'expliquer qu'il faut optimiser mais avec des outils, comme un profileur.

Vous devez constamment profiler et profiler la mémoire de votre application une fois l'architecture très basique posée.

Le profilage ne vous aidera pas seulement à augmenter la vitesse, il vous aidera à trouver des bogues. Si votre programme change soudainement radicalement de vitesse, c'est généralement à cause d'un bug. Si vous ne profilez pas, cela peut passer inaperçu.

L'astuce pour optimiser est de le faire par conception. N'attendez pas la dernière minute. Assurez-vous que la conception de votre programme vous donne les performances dont vous avez besoin sans vraiment de mauvais tours de boucle intérieure.

Jonathan Fischoff
la source
1

Pour mon projet, j'applique généralement des optimisations TRÈS nécessaires dans mon moteur de base. Par exemple, j'aime toujours implémenter une bonne implémentation SIMD solide en utilisant SSE2 et 3DNow! Cela garantit que mes calculs en virgule flottante sont en phase avec l'endroit où je veux qu'ils soient. Une autre bonne pratique consiste à prendre l'habitude des optimisations lorsque vous codez au lieu de revenir en arrière. La plupart du temps, ces petites pratiques prennent autant de temps que ce que vous codiez de toute façon. Avant de coder une fonctionnalité, assurez-vous de rechercher le moyen le plus efficace de le faire.

En bout de ligne, à mon avis, son plus dur pour rendre votre code plus efficace après qu'il est déjà nul.

Krankzinnig
la source
0

Je dirais que la façon la plus simple serait d'utiliser votre bon sens - si quelque chose semble fonctionner lentement, alors jetez-y un œil. Voyez s'il s'agit d'un goulot d'étranglement.
Utilisez un profileur pour voir les fonctions de vitesse et la fréquence à laquelle elles sont appelées.
Il est absolument inutile d'optimiser ou de passer du temps à essayer d'optimiser quelque chose qui n'en a pas besoin.

Le canard communiste
la source
0

Si votre code s'exécute lentement, exécutez un profileur et voyez exactement ce qui le ralentit. Ou vous pouvez être proactif et avoir déjà un profileur en cours d'exécution avant de commencer à remarquer des problèmes de performances.

Vous voudrez optimiser lorsque votre taux de rafraîchissement diminue au point que le jeu commence à souffrir. Votre coupable le plus probable sera que votre processeur est trop utilisé (100%).

Bryan Denny
la source
Je dirais que le GPU est tout aussi probable que le CPU. En effet, selon la façon dont les choses sont étroitement couplées, il est tout à fait possible d'être fortement lié au CPU dans la moitié du cadre et fortement au GPU dans l'autre moitié. Le profilage muet peut même montrer une utilisation inférieure à 100% dans les deux cas. Assurez-vous que votre profilage est suffisamment fin pour le montrer (mais pas assez fin pour être intrusif!)
JasonD
0

Vous devez optimiser votre code ... aussi souvent que nécessaire.

Ce que j'ai fait dans le passé, c'est simplement de lancer le jeu en continu avec le profilage activé (au moins un compteur de fréquence d'images à l'écran à tout moment). Si le jeu devient lent (en dessous de votre cadence d'images cible sur votre machine min spec, par exemple), allumez le profileur et voyez si des points chauds apparaissent.

Parfois, ce n'est pas le code. Beaucoup de problèmes que j'ai rencontrés dans le passé ont été orientés vers le GPU (d'accord, c'était sur l'iPhone). Problèmes de remplissage, trop d'appels de tirage, pas assez de géométrie par lots, shaders inefficaces ...

Outre les algorithmes inefficaces pour les problèmes difficiles (c.-à-d. Recherche de chemin, physique), j'ai très rarement rencontré des problèmes où le code lui-même était le coupable. Et ces problèmes difficiles devraient être des choses que vous consacrez beaucoup d'efforts à obtenir l'algorithme correct et à ne pas vous soucier de petites choses.

Tetrad
la source
0

Pour moi, c'est le meilleur modèle de données bien préparé. Et optimisation - avant le grand pas en avant. Je veux dire avant de commencer à mettre en œuvre quelque chose de grand nouveau. L'autre raison de l'optimisation est lorsque je perds le contrôle des ressources, l'application a besoin de beaucoup de charge CPU / GPU ou de mémoire et je ne sais pas pourquoi :) ou c'est trop.

samboush
la source