Je ne peux pas, pour la vie de moi, me rappeler exactement ce que notre professeur a dit ce jour-là et j'espère que vous le saurez probablement.
Le module est "Structures de données et algorithmes" et il nous a dit quelque chose comme:
L'instruction
if
est la [chose] la plus chère. [quelque chose] enregistre [quelque chose].
Oui, j'ai une horrible mémoire et je suis vraiment vraiment désolé, mais je google depuis des heures et rien ne s'est produit. Des idées?
Au niveau le plus bas (dans le matériel), oui, si s sont chers. Pour comprendre pourquoi, vous devez comprendre comment pipelines fonctionne.
L'instruction en cours à exécuter est stockée dans quelque chose généralement appelé pointeur d'instruction (IP) ou compteur de programme (PC); ces termes sont synonymes, mais des termes différents sont utilisés avec des architectures différentes. Pour la plupart des instructions, le PC de l'instruction suivante n'est que le PC actuel plus la longueur de l'instruction actuelle. Pour la plupart des architectures RISC, les instructions sont toutes de longueur constante, de sorte que le PC peut être incrémenté d'une quantité constante. Pour les architectures CISC telles que x86, les instructions peuvent être de longueur variable, de sorte que la logique qui décode l'instruction doit déterminer la durée de l'instruction actuelle pour trouver l'emplacement de l'instruction suivante.
Pour les instructions branch, cependant, la prochaine instruction à exécuter n'est pas le prochain emplacement après l'instruction en cours. Les branches sont des gotos - elles indiquent au processeur où se trouve la prochaine instruction. Les branches peuvent être conditionnelles ou inconditionnelles, et l'emplacement cible peut être fixe ou calculé.
Le conditionnel et l'inconditionnel est facile à comprendre - une branche conditionnelle n'est prise que si une certaine condition est vérifiée (par exemple, si un nombre est égal à un autre); si la branche n'est pas prise, le contrôle passe à l'instruction suivante après la branche comme d'habitude. Pour les branches inconditionnelles, la branche est toujours prise. Les branches conditionnelles apparaissent dans les instructions if
et les tests de contrôle des boucles for
et while
. Les branches inconditionnelles apparaissent dans des boucles infinies, des appels de fonction, des retours de fonction, des instructions break
et continue
, la fameuse instruction goto
, et bien d'autres (ces listes sont loin d'être exhaustives) .
La cible des succursales est un autre problème important. La plupart des branches ont une cible de branche fixe - elles vont à un emplacement spécifique dans le code qui est fixé au moment de la compilation. Cela inclut les instructions if
, les boucles de toutes sortes, les appels de fonction réguliers et bien d'autres. Calculé les branches calculent la cible de la branche lors de l'exécution. Cela inclut les instructions switch
(parfois), le retour d'une fonction, les appels de fonction virtuelle et les appels de pointeur de fonction.
Alors qu'est-ce que tout cela signifie pour la performance? Lorsque le processeur voit une instruction de branchement apparaître dans son pipeline, il doit trouver comment continuer à remplir son pipeline. Afin de comprendre quelles instructions viennent après la branche dans le flux de programme, il doit savoir deux choses: (1) si la branche sera prise et (2) la cible de la branche. Comprendre cela s'appelle prédiction de branche , et c'est un problème difficile. Si le processeur devine correctement, le programme continue à pleine vitesse. Si, à la place, le processeur devine incorrectement, il a juste passé du temps à calculer la mauvaise chose. Il doit maintenant vider son pipeline et le recharger avec les instructions du chemin d'exécution correct. Conclusion: un grand succès de performance.
Ainsi, la raison pour laquelle si les déclarations sont coûteuses est due à des erreurs de prédiction de branche . Ce n'est qu'au niveau le plus bas. Si vous écrivez du code de haut niveau, vous n'avez pas du tout à vous soucier de ces détails. Vous ne devez vous en soucier que si vous écrivez du code extrêmement critique en termes de performances en C ou en Assembly. Si tel est le cas, l'écriture de code sans branche peut souvent être supérieure au code qui branche, même si plusieurs instructions supplémentaires sont nécessaires. Il y a quelques trucs sympas de twiddling que vous pouvez faire pour calculer des choses telles que abs()
, min()
et max()
sans branchement.
"Cher" est un terme très relatif, en particulier en relation avec une instruction "if
" puisque vous devez également prendre en compte le coût de la condition. Cela peut aller de quelques courtes instructions de processeur à tester le résultat d'une fonction qui appelle une base de données distante.
Je ne m'en inquiéterais pas. À moins que vous ne fassiez de la programmation intégrée, vous ne devriez probablement pas vous soucier du coût de "if
". Pour la plupart des programmeurs, cela ne va tout simplement pas jamais être le facteur déterminant dans les performances de votre application.
Les branches, en particulier sur les microprocesseurs à architecture RISC, sont parmi les instructions les plus coûteuses. En effet, sur de nombreuses architectures, le compilateur prédit le chemin d'exécution le plus probable et place ces instructions dans l'exécutable, de sorte qu'elles seront déjà dans le cache du processeur lorsque la branche se produit. Si la branche va dans l'autre sens, elle doit retourner dans la mémoire principale et récupérer les nouvelles instructions - c'est assez cher. Sur de nombreuses architectures RISC, toutes les instructions sont un cycle sauf pour la branche (qui est souvent 2 cycles). Nous ne parlons pas d'un coût majeur ici, alors ne vous inquiétez pas. De plus, le compilateur optimisera mieux que vous 99% du temps :) L'une des choses vraiment impressionnantes à propos de l'architecture EPIC (Itanium en est un exemple) est qu'il met en cache (et commence à traiter) les instructions des deux côtés de la branche, puis supprime l'ensemble dont il n'a pas besoin une fois que le résultat de la branche est connu. Cela permet d'économiser l'accès mémoire supplémentaire d'une architecture typique dans le cas où elle se ramifie le long du chemin imprévu.
Consultez l'article Better Performance Through Branch Elimination on Cell Performance. Un autre amusant est ce post sur les sélections sans branche sur le blog de détection des collisions en temps réel.
En plus des excellentes réponses déjà publiées en réponse à cette question, je voudrais rappeler que bien que les instructions "si" soient considérées comme des opérations de bas niveau coûteuses, essayer d'utiliser des techniques de programmation sans branche dans un environnement de niveau supérieur , comme un langage de script ou une couche de logique métier (quelle que soit la langue), peut être ridiculement inapproprié.
La grande majorité du temps, les programmes doivent d'abord être écrits pour plus de clarté et optimisés pour les performances ensuite. Il existe de nombreux domaines problématiques où les performances sont primordiales, mais le simple fait est que la plupart des développeurs n'écrivent pas de modules à utiliser au cœur d'un moteur de rendu ou d'une simulation de dynamique des fluides haute performance qui dure des semaines. Lorsque la priorité absolue est que votre solution "fonctionne tout simplement", la dernière chose à laquelle vous pensez devrait être de savoir si vous pouvez ou non économiser sur les frais généraux d'une instruction conditionnelle dans votre code.
Au niveau le plus bas possible, if
comprend (après avoir calculé tous les prérequis spécifiques à l'application pour un if
particulier):
Coûts associés à cela:
Reson pourquoi les sauts sont chers:
Pour résumer:
if
en soi est pas lent. La lenteur est toujours relative, je parie pour ma vie que vous n'avez jamais ressenti les "frais généraux" d'une instruction if. Si vous allez créer un code haute performance, vous voudrez peut-être éviter les branches de toute façon. Ce qui rend if
lent, c'est que le processeur précharge le code après if
sur la base d'heuristiques et autres joyeusetés. Cela empêchera également les pipelines d'exécuter du code directement après l'instruction de branche if
dans le code machine, car le processeur ne sait pas encore quel chemin sera emprunté (dans un processeur pipeliné, plusieurs instructions sont entrelacées et exécutées) . Le code exécuté pourrait devoir être exécuté à l'envers (si l'autre branche a été prise. Elle s'appelle branch misprediction
) ou noop
doivent être remplis à ces endroits afin que cela ne se produise pas.
Si if
est mauvais, alors switch
est mauvais aussi, et &&
, ||
aussi. Ne t'en fais pas.
Peut-être que la ramification tue l'instruction de pré-lecture des instructions du processeur?
Les processeurs modernes ont de longs pipelines d'exécution, ce qui signifie que plusieurs instructions sont exécutées en plusieurs étapes en même temps. Ils peuvent ne pas toujours connaître le résultat d'une instruction lorsque la prochaine commence à s'exécuter. Quand ils rencontrent un saut conditionnel (si), ils doivent parfois attendre que le pipeline soit vide avant de savoir dans quelle direction le pointeur d'instruction doit aller.
Je le vois comme un long train de marchandises. Il peut transporter beaucoup de marchandises rapidement en ligne droite, mais il tourne mal.
Le Pentium 4 (Prescott) avait un pipeline long et célèbre de 31 étages.
Plus sur Wikipedia
Comme plusieurs l'ont souligné, les branches conditionnelles peuvent être très lentes sur un ordinateur moderne.
Cela étant dit, il y a beaucoup de branches conditionnelles qui ne vivent pas dans les instructions if, vous ne pouvez pas toujours dire ce que le compilateur proposera, et vous inquiéter du temps que prendront les instructions de base est pratiquement toujours la mauvaise chose faire. (Si vous pouvez dire ce que le compilateur va générer de manière fiable, vous ne disposez peut-être pas d'un bon compilateur d'optimisation.)
Notez également que l'intérieur d'une boucle est pas nécessairement très cher.
Le CPU moderne suppose lors de la première visite d'une instruction if, que le "if-body" doit être pris (ou dit dans l'autre sens: il suppose également qu'un loop-body doit être pris plusieurs fois) (*). Lors de la deuxième visite et des visites ultérieures, il (le CPU) peut peut-être consulter le Tableau d'historique des branches , et voir comment la condition était la dernière fois (était-ce vrai? était-ce faux?). S'il était faux la dernière fois, alors l'exécution spéculative se poursuivra vers le "sinon" de l'if, ou au-delà de la boucle.
(*) La règle est en fait " branche avant non prise, branche arrière prise ". Dans une instruction if, il y a seulement un saut [vers l'avant] (jusqu'au point après le if-body ) si la condition est fausse (rappelez-vous: le CPU suppose de toute façon ne pas prendre de branche/saut), mais dans une boucle, il y a peut-être une branche avant à la position après la boucle (à ne pas prendre), et une arrière branche lors de la répétition (à prendre).
C'est aussi l'une des raisons pour lesquelles un appel à une fonction virtuelle ou un appel de pointeur de fonction n'est pas pire que beaucoup le supposent ( http://phresnel.org/blog/ )
Les processeurs sont profondément pipelinés. Toute instruction de branchement (si/pour/pendant/commutateur/etc) signifie que le CPU ne sait pas vraiment quelle instruction charger et exécuter ensuite.
Le CPU se bloque en attendant de savoir quoi faire, ou le CPU fait une supposition. Dans le cas d'un processeur plus ancien, ou si la supposition est fausse, vous devrez subir un blocage du pipeline pendant qu'il va et charge l'instruction correcte. Selon le processeur, cela peut représenter jusqu'à 10 à 20 instructions de décrochage.
Les processeurs modernes essaient d'éviter cela en effectuant une bonne prédiction de branche et en exécutant plusieurs chemins en même temps, et en ne conservant que le chemin réel. Cela aide beaucoup, mais ne peut aller si loin.
Bonne chance dans la classe.
De plus, si vous devez vous en soucier dans la vraie vie, vous faites probablement de la conception de système d'exploitation, des graphiques en temps réel, du calcul scientifique ou quelque chose de similaire lié au processeur. Profil avant de s'inquiéter.
Écrivez vos programmes de la manière la plus claire, la plus simple et la plus propre qui n'est évidemment pas inefficace. Cela fait le meilleur usage de la ressource la plus chère, vous. Que ce soit l'écriture ou le débogage ultérieur (nécessite la compréhension) du programme. Si les performances ne sont pas suffisantes, mesure où se trouvent les goulots d'étranglement et voyez comment les atténuer. Ce n'est que dans des cas extrêmement rares que vous devrez vous soucier des instructions individuelles (source) lors de cette opération. La performance consiste à sélectionner les bons algorithmes et structures de données en première ligne, à programmer avec soin, à obtenir une machine suffisamment rapide. Utilisez un bon compilateur, vous seriez surpris de voir le type de restructuration de code d'un compilateur moderne. La restructuration du code de performance est une sorte de mesure de dernier recours, le code devient plus complexe (donc plus bogué), plus difficile à modifier, et donc plus cher.
J'ai eu cet argument avec un de mes amis une fois. Il utilisait un algorithme de cercle très naïf, mais prétendait qu'il était plus rapide que le mien (le genre qui ne calcule que le 1/8 du cercle) parce que le mien utilisait if. En fin de compte, l'instruction if a été remplacée par sqrt et, en quelque sorte, c'était plus rapide. Peut-être parce que le FPU a intégré sqrt?
Certains processeurs (comme X86) fournissent une prédiction de branche au niveau de la programmation pour éviter une telle latence de prédiction de branche.
Certains compilateurs les exposent (comme GCC) comme une extension aux langages de programmation de niveau supérieur (comme C/C++).