En supposant que vous ayez déjà le meilleur algorithme, quelles solutions de bas niveau pouvez-vous proposer pour extraire les dernières gouttes de cadence douce en code C ++?
Il va sans dire que ces conseils ne s'appliquent qu'à la section de code critique que vous avez déjà mise en surbrillance dans votre profileur. Toutefois, ils doivent constituer des améliorations non structurelles de bas niveau. J'ai semé un exemple.
c++
optimization
réduction
la source
la source
Réponses:
Optimisez votre mise en page de données! (Ceci s'applique à plus de langages que juste C ++)
Vous pouvez aller assez en profondeur en adaptant cela spécifiquement à vos données, à votre processeur, à la gestion multi-cœur, etc. Mais le concept de base est le suivant:
Lorsque vous traitez des éléments en boucle étroite, vous souhaitez que les données de chaque itération soient aussi petites que possible et aussi proches que possible en mémoire. Cela signifie que l’idéal est un tableau ou un vecteur d’objets (pas de pointeurs) contenant uniquement les données nécessaires au calcul.
Ainsi, lorsque la CPU récupère les données pour la première itération de votre boucle, les différentes itérations suivantes de données sont chargées dans le cache.
Vraiment le processeur est rapide et le compilateur est bon. Vous ne pouvez pas vraiment faire grand chose en utilisant des instructions moins nombreuses et plus rapides. La cohérence du cache en est la base (c'est un article aléatoire que j'ai cherché sur Google - il contient un bon exemple pour obtenir la cohérence du cache pour un algorithme qui ne traite pas simplement les données de manière linéaire).
la source
Un conseil très, très bas, mais qui peut être utile:
La plupart des compilateurs prennent en charge certaines formes d'indices conditionnels explicites. GCC a une fonction appelée __builtin_expect qui vous permet d'informer le compilateur de la valeur probable d'un résultat. GCC peut utiliser ces données pour optimiser les conditions conditionnelles afin qu'elles s'exécutent aussi rapidement que possible dans le cas prévu, avec une exécution légèrement plus lente dans le cas imprévu.
J'ai constaté une accélération de 10 à 20% avec une utilisation appropriée de cette fonctionnalité.
la source
La première chose que vous devez comprendre est le matériel que vous utilisez. Comment gère-t-il les branches? Qu'en est-il de la mise en cache? At-il un jeu d'instructions SIMD? Combien de processeurs peut-il utiliser? Doit-il partager le temps processeur avec autre chose?
Vous pouvez résoudre le même problème de manières très différentes - même votre choix d'algorithme doit dépendre du matériel. Dans certains cas, O (N) peut fonctionner plus lentement que O (NlogN) (en fonction de la mise en œuvre).
En tant que vue d’ensemble de l’optimisation, la première chose à faire est d’examiner exactement quels problèmes et quelles données vous essayez de résoudre. Optimisez ensuite pour cela. Si vous voulez des performances extrêmes, oubliez les solutions génériques - vous pouvez personnaliser tout ce qui ne correspond pas à votre cas le plus utilisé.
Puis profil. Profil, profil, profil. Examinez l'utilisation de la mémoire, examinez les pénalités de branchement, examinez la surcharge des appels de fonction, examinez l'utilisation du pipeline. Déterminez ce qui ralentit votre code. Il s’agit probablement de l’accès aux données (j’ai écrit un article intitulé "The Latency Elephant" sur les frais généraux liés à l’accès aux données - google it. Je ne peux pas poster 2 liens ici car je n’ai pas assez de "réputation"), examinez de près cela et optimisez ensuite la disposition de vos données (de superbes baies plates et homogènes sont géniales ) et votre accès aux données (pré-extraction si possible).
Une fois que vous avez minimisé les frais généraux du sous-système de mémoire, essayez de déterminer si les instructions constituent maintenant le goulot d'étranglement (espérons-le), puis examinez les implémentations SIMD de votre algorithme - Les implémentations Structure-of-Arrays (SoA) peuvent être très data et cache d'instruction efficace. Si SIMD ne correspond pas à votre problème, des codages intrinsèques et de niveau assembleur peuvent être nécessaires.
Si vous avez encore besoin de plus de vitesse, passez en parallèle. Si vous avez l'avantage de fonctionner sur une PS3, alors les SPU sont vos amis. Utilisez-les, aimez-les. Si vous avez déjà écrit une solution SIMD, vous obtiendrez un avantage considérable en passant à SPU.
Et ensuite, profilez un peu plus. Testez dans des scénarios de jeu - ce code est-il toujours le goulot d'étranglement? Pouvez-vous changer la façon dont ce code est utilisé à un niveau supérieur pour minimiser son utilisation (en fait, cela devrait être votre première étape)? Pouvez-vous différer les calculs sur plusieurs images?
Quelle que soit la plate-forme sur laquelle vous vous trouvez, renseignez-vous le plus possible sur le matériel et les profileurs disponibles. Ne présumez pas que vous connaissez le goulot d'étranglement - trouvez-le avec votre profileur. Et assurez-vous d'avoir une heuristique pour déterminer si vous avez réellement accéléré votre jeu.
Et puis profilez à nouveau.
la source
Première étape: réfléchissez bien à vos données par rapport à vos algorithmes. O (log n) n'est pas toujours plus rapide que O (n). Exemple simple: il est souvent préférable de remplacer une table de hachage avec seulement quelques clés par une recherche linéaire.
Deuxième étape: regardez l'assemblage généré. C ++ apporte beaucoup de code implicite à la table. Parfois, il se faufile sur vous sans que vous le sachiez.
Mais en supposant que c’est vraiment le moment de pédaler du métal: Profile. Sérieusement. Appliquer au hasard des "astuces de performance" est aussi susceptible de faire mal que d’aider.
Ensuite, tout dépend de vos goulots d'étranglement.
cache de données manquantes => optimisez la disposition de vos données. Voici un bon point de départ: http://gamesfromwithin.com/data-oriented-design
code cache misses => Examinez les appels de fonctions virtuelles, la profondeur excessive de la pile d’appel, etc. Une cause fréquente de mauvaises performances est la croyance erronée que les classes de base doivent être virtuelles.
Autres puits de performance C ++ courants:
Toutes les réponses ci-dessus sont immédiatement évidentes lorsque vous regardez l’ensemble, voyez donc ci-dessus;)
la source
Supprimer les branches inutiles
Sur certaines plates-formes et avec certains compilateurs, les branches peuvent jeter tout votre pipeline. Même des blocs if si insignifiants peuvent coûter cher.
L'architecture PowerPC (PS3 / X360) offre l'instruction de sélection à virgule flottante,
fsel
. Ceci peut être utilisé à la place d'une branche si les blocs sont de simples affectations:Devient:
Lorsque le premier paramètre est supérieur ou égal à 0, le deuxième paramètre est renvoyé, sinon le troisième.
Le prix à payer pour perdre la branche est que le bloc if {} et le bloc else {} seront exécutés. Par conséquent, si l'opération est coûteuse ou supprime un pointeur NULL, cette optimisation n'est pas appropriée.
Parfois, votre compilateur a déjà effectué ce travail, alors vérifiez d'abord votre assemblage.
Voici plus d'informations sur les branches et le fsel:
http://assemblyrequired.crashworks.org/tag/intrinsics/
la source
Évitez à tout prix les accès à la mémoire, en particulier les plus aléatoires.
C'est la chose la plus importante à optimiser pour les processeurs modernes. Vous pouvez faire un tas de calculs arithmétiques et même beaucoup de branches mal prédites pendant que vous attendez des données de la RAM.
Vous pouvez également lire cette règle dans l’inverse: faites autant de calculs que possible entre les accès mémoire.
la source
Utilisez les éléments intrinsèques du compilateur.
Assurez-vous que le compilateur génère l'assemblage le plus efficace pour certaines opérations en utilisant intrinsics - des constructions ressemblant à des appels de fonction que le compilateur transforme en assemblage optimisé:
Voici une référence pour Visual Studio , et en voici une pour GCC
la source
Supprimer les appels de fonction virtuels inutiles
L'envoi d'une fonction virtuelle peut être très lent. Cet article donne une bonne explication de pourquoi. Si possible, évitez-les pour les fonctions appelées plusieurs fois par image.
Vous pouvez le faire de plusieurs manières. Parfois, vous pouvez simplement réécrire les classes pour ne pas avoir besoin d'héritage - peut-être qu'il se trouve que MachineGun est la seule sous-classe de Weapon et que vous pouvez les fusionner.
Vous pouvez utiliser des modèles pour remplacer le polymorphisme au moment de la compilation par un polymorphisme au moment de la compilation. Cela ne fonctionne que si vous connaissez le sous-type de vos objets au moment de l'exécution et peut être une réécriture majeure.
la source
Mon principe de base est le suivant: ne faites rien qui ne soit pas nécessaire .
Si vous avez constaté qu'une fonction particulière est un goulot d'étranglement, vous pouvez l'optimiser - ou vous pouvez essayer de l'empêcher d'appeler cette fonction.
Cela ne signifie pas nécessairement que vous utilisez un mauvais algorithme. Cela signifie peut-être que vous exécutez des calculs pour chaque image pouvant être mise en cache pendant un court instant (ou entièrement précalculée), par exemple.
J'essaie toujours cette approche avant toute tentative d'optimisation à très bas niveau.
la source
Utilisez SIMD (par SSE), si vous ne le faites pas déjà. Gamasutra a un bel article à ce sujet . Vous pouvez télécharger le code source à partir de la bibliothèque présentée à la fin de l'article.
la source
Réduisez au minimum les chaînes de dépendance afin de mieux utiliser la ligne de commande du processeur.
Dans des cas simples, le compilateur peut le faire pour vous si vous activez le déroulement de la boucle. Cependant, il ne le fait souvent pas, en particulier lorsque des flottants sont impliqués, car la réorganisation des expressions modifie le résultat.
Exemple:
la source
Ne négligez pas votre compilateur - si vous utilisez gcc sur Intel, vous pouvez facilement obtenir un gain de performances en passant au compilateur Intel C / C ++, par exemple. Si vous ciblez une plate-forme ARM, consultez le compilateur commercial d’ARM. Si vous êtes sur l'iPhone, Apple vient d'autoriser l'utilisation de Clang à partir du SDK iOS 4.0.
L’un des problèmes que vous rencontrerez probablement avec l’optimisation, en particulier sur le x86, est que beaucoup de choses intuitives finissent par s’opposer à vous sur les implémentations de processeurs modernes. Malheureusement pour la plupart d'entre nous, la capacité d'optimiser le compilateur a disparu depuis longtemps. Le compilateur peut planifier des instructions dans le flux en fonction de sa propre connaissance interne du processeur. En outre, le processeur peut également reprogrammer des instructions en fonction de ses propres besoins. Même si vous pensez à une méthode optimale pour organiser une méthode, il est probable que le compilateur ou le processeur l'ait déjà conçue seule et qu'elle a déjà effectué cette optimisation.
Mon meilleur conseil serait d'ignorer les optimisations de bas niveau et de se concentrer sur les optimisations de niveau supérieur. Le compilateur et la CPU ne peuvent pas changer votre algorithme d'un algorithme O (n ^ 2) à un algorithme O (1), quelle que soit leur qualité. Cela va vous obliger à regarder exactement ce que vous essayez de faire et à trouver un meilleur moyen de le faire. Laissez le compilateur et la CPU se soucier du niveau bas et concentrez-vous sur les niveaux moyen à élevé.
la source
Le mot-clé restrict est potentiellement pratique, notamment dans les cas où vous devez manipuler des objets avec des pointeurs. Cela permet au compilateur de supposer que l’objet pointé ne sera pas modifié d’une autre manière, ce qui lui permettra d’optimiser de manière plus agressive, par exemple en conservant des parties de l’objet dans des registres ou en réorganisant les lectures et les écritures.
Une bonne chose à propos du mot clé est qu'il s'agit d'un conseil que vous pouvez appliquer une fois pour voir les avantages qui en découlent sans modifier votre algorithme. Le mauvais côté est que si vous l'utilisez au mauvais endroit, vous pourriez voir des données corrompues. Mais en général, il est assez facile de repérer où il est légitime de l'utiliser - c'est l'un des rares exemples dans lesquels on peut raisonnablement s'attendre à ce que le programmeur en sache plus que ce que le compilateur peut supposer en toute sécurité, ce qui explique pourquoi le mot clé a été introduit.
Techniquement, la «restriction» n'existe pas en C ++ standard, mais des équivalents spécifiques à une plate-forme sont disponibles pour la plupart des compilateurs C ++.
Voir aussi: http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html
la source
Const tout!
Plus vous donnez d'informations au compilateur sur les données, meilleures sont les optimisations (du moins d'après mon expérience).
devient;
Le compilateur sait maintenant que le pointeur x ne va pas changer et que les données qu'il pointe ne changeront pas non plus.
L’autre avantage supplémentaire est que vous pouvez réduire le nombre de bogues accidentels, en vous arrêtant (vous-même ou d’autres) en modifiant des choses qu’ils ne devraient pas.
la source
const
n'améliore pas les optimisations du compilateur. Certes, le compilateur peut générer un meilleur code s'il sait qu'une variable ne changera pas, maisconst
ne fournit pas une garantie suffisamment forte.Le plus souvent, le meilleur moyen d'obtenir des performances est de changer votre algorithme. Moins la mise en œuvre est générale, plus vous pouvez vous rapprocher du métal.
En supposant que cela ait été fait ...
Si le code est vraiment essentiel, essayez d'éviter les lectures en mémoire, évitez de calculer des éléments qui peuvent être précalculés (bien qu'aucune table de recherche ne viole la règle 1). Sachez ce que fait votre algorithme et écrivez-le de manière à ce que le compilateur le sache également. Vérifiez l'assemblage pour vous en assurer.
Évitez les oublis de cache. Traitement par lots autant que vous le pouvez. Évitez les fonctions virtuelles et autres indirections.
En fin de compte, tout mesurer. Les règles changent tout le temps. Ce qui accélérait le code il y a 3 ans le ralentit maintenant. Un bon exemple est «utilisez des fonctions mathématiques doubles au lieu de versions flottantes». Je ne l'aurais pas compris si je ne l'avais pas lu.
J'ai oublié - les constructeurs par défaut n'ont pas besoin d'intialiser vos variables, ou si vous insistez, créez au moins aussi des constructeurs qui ne le font pas. Soyez conscient des choses qui ne figurent pas dans les profils. Lorsque vous perdez un cycle inutile par ligne de code, rien n’apparaîtra dans votre profileur, mais vous perdrez beaucoup de cycles dans l’ensemble. Encore une fois, sachez ce que fait votre code. Rendez votre fonction principale maigre au lieu d'être infaillible. Des versions à toute épreuve peuvent être appelées si nécessaire, mais ne sont pas toujours nécessaires. La polyvalence a un prix - la performance en est une.
Édité pour expliquer pourquoi aucune initialisation par défaut: Beaucoup de code dit: Vector3 bla; bla = DoQuelque chose ();
L'initialisation dans le constructeur est une perte de temps. De plus, dans ce cas, le temps perdu est peu important (effacement du vecteur probablement), mais si vos programmeurs le font habituellement, cela s’ajoute. En outre, beaucoup de fonctions créent un temporaire (pensez opérateurs surchargés), qui est initialisé à zéro et attribué immédiatement après. Les cycles perdus masqués qui sont trop petits pour voir une pointe dans votre profileur, mais les cycles de fond perdu dans votre base de code. En outre, certaines personnes font beaucoup plus dans les constructeurs (ce qui est évidemment un non-non). J'ai vu des gains de plusieurs millisecondes avec une variable non utilisée dans laquelle le constructeur était un peu lourd. Dès que le constructeur provoque des effets secondaires, le compilateur ne pourra pas l'optimiser. Par conséquent, à moins que vous n'utilisiez jamais le code ci-dessus, je préfère un constructeur non-initialisant ou, comme je l'ai dit,
Vector3 bla (noInit); bla = doQuelque chose ();
la source
const Vector3 = doSomething()
? Ensuite, l’optimisation de la valeur de retour peut commencer et probablement éliminer une ou deux tâches.Réduire l'évaluation de l'expression booléenne
Celui-ci est vraiment désespéré, car c'est une modification très subtile mais dangereuse de votre code. Toutefois, si votre condition est évaluée un nombre de fois excessif, vous pouvez réduire les frais généraux liés à l'évaluation booléenne en utilisant plutôt des opérateurs au niveau du bit. Alors:
Devient:
Utiliser l'arithmétique entière à la place. Si vos foos et barres sont des constantes ou évaluées avant if (), cela pourrait être plus rapide que la version booléenne normale.
En prime, la version arithmétique a moins de branches que la version booléenne classique. Ce qui est une autre façon d' optimiser .
Le gros inconvénient est que vous perdez une évaluation paresseuse - le bloc entier est évalué, vous ne pouvez donc pas le faire
foo != NULL & foo->dereference()
. Pour cette raison, on peut soutenir que cela est difficile à maintenir et que le compromis peut être trop important.la source
Gardez un œil sur votre utilisation de la pile
Tout ce que vous ajoutez à la pile est une poussée et une construction supplémentaires quand une fonction est appelée. Lorsqu'une grande quantité d'espace de pile est requise, il peut parfois être avantageux d'allouer de la mémoire de travail à l'avance. Si la plate-forme sur laquelle vous travaillez est dotée d'une mémoire vive rapide à utiliser, tant mieux!
la source