web-dev-qa-db-fra.com

"SI" est-il cher?

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?

84
pek

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.

168
Adam Rosenfield

"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.

16
Joel Coehoorn

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.

14
rmeador

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.

10
Parappa

Au niveau le plus bas possible, if comprend (après avoir calculé tous les prérequis spécifiques à l'application pour un if particulier):

  • quelques instructions de test
  • sautez à un endroit dans le code si le test réussit, continuez dans le cas contraire.

Coûts associés à cela:

  • une comparaison de bas niveau - généralement 1 opération de processeur, super bon marché
  • saut potentiel - qui peut être coûteux

Reson pourquoi les sauts sont chers:

  • vous pouvez passer au code arbitraire qui vit n'importe où dans la mémoire, s'il s'avère qu'il n'est pas mis en cache par le processeur - nous avons un problème, car nous devons accéder à la mémoire principale, qui est plus lente
  • les processeurs modernes font la prédiction de branche. Ils essaient de deviner si cela réussira ou non et exécutent le code à l'avance dans le pipeline, alors accélérez les choses. Si la prédiction échoue, tous les calculs effectués à l'avance par pipeline doivent être invalidés. C'est aussi une opération coûteuse

Pour résumer:

  • Si cela peut coûter cher, si vous vous souciez vraiment, vraiment, de la performance.
  • Vous devriez vous en soucier si et seulement si vous écrivez un raytracer en temps réel ou une simulation biologique ou quelque chose de similaire. Il n'y a aucune raison de s'en soucier dans la plupart du monde réel.
7
Marcin

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.

7

Peut-être que la ramification tue l'instruction de pré-lecture des instructions du processeur?

6
activout.se

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

6
Guge

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.)

4
David Thornley

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/ )

3
Sebastian Mach

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.

3
tfinniga

É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.

1
vonbrand

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?

0
Demur Rumed

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++).

Voir macros probables ()/improbables () dans le noyau Linux - comment fonctionnent-elles? Quel est leur avantage? .

0
Arunprasad Rajkumar