Notre professeur d'informatique a dit un jour que pour une raison quelconque, il est plus efficace de compter à rebours que de compter à rebours. Par exemple, si vous devez utiliser une boucle FOR et que l'index de la boucle n'est pas utilisé quelque part (comme imprimer une ligne de N * à l'écran), je veux dire ce code comme ceci:
for (i = N; i >= 0; i--)
putchar('*');
est mieux que:
for (i = 0; i < N; i++)
putchar('*');
Est-ce vraiment vrai? Et si oui, est-ce que quelqu'un sait pourquoi?
c
performance
loops
Bob
la source
la source
putchar
utilise 99,9999% du temps (donner ou prendre).i
n'est pas signée, la première boucle est infinie?Réponses:
Dans les temps anciens, lorsque les ordinateurs étaient encore découpés à la main dans de la silice fondue, lorsque des microcontrôleurs 8 bits parcouraient la Terre et que votre professeur était jeune (ou que l'enseignant de votre professeur était jeune), il y avait une instruction machine commune appelée décrémentation et saut. si zéro (DSZ). Les programmeurs d'assemblage Hotshot ont utilisé cette instruction pour implémenter des boucles. Les machines plus récentes recevaient des instructions plus sophistiquées, mais il y avait encore pas mal de processeurs sur lesquels il était moins cher de comparer quelque chose avec zéro que de comparer avec autre chose. (C'est vrai même sur certaines machines RISC modernes, comme PPC ou SPARC, qui réservent un registre entier pour toujours être nul.)
Donc, si vous configurez vos boucles pour les comparer à zéro au lieu de
N
, que pourrait-il se passer?Ces différences sont-elles susceptibles d'entraîner une amélioration mesurable sur des programmes réels sur un processeur moderne en panne? Hautement improbable. En fait, je serais impressionné si vous pouviez montrer une amélioration mesurable même sur un microbenchmark.
Résumé: Je frappe votre professeur à l'envers de la tête! Vous ne devriez pas apprendre des pseudo-faits obsolètes sur la façon d'organiser les boucles. Vous devriez apprendre que la chose la plus importante à propos des boucles est d'être sûr qu'elles se terminent , produisent des réponses correctes et sont faciles à lire . J'aimerais que votre professeur se concentre sur les choses importantes et non sur la mythologie.
la source
putchar
prend plusieurs ordres de grandeur plus longtemps que la surcharge de la boucle.j=N-i
montre que les deux boucles sont équivalentes.Voici ce qui peut arriver sur certains matériels en fonction de ce que le compilateur peut déduire de la plage des nombres que vous utilisez: avec la boucle d'incrémentation, vous devez tester à
i<N
chaque fois le tour de la boucle. Pour la version décrémentante, l'indicateur de retenue (défini comme effet secondaire de la soustraction) peut automatiquement vous indiquer sii>=0
. Cela économise un test par fois autour de la boucle.En réalité, sur le matériel de processeur en pipeline moderne, ce truc est presque certainement hors de propos car il n'y a pas un simple mappage 1-1 des instructions aux cycles d'horloge. (Bien que je puisse imaginer que cela se produirait si vous faisiez des choses comme générer des signaux vidéo minutés avec précision à partir d'un microcontrôleur. Mais alors vous écririez de toute façon en langage d'assemblage.)
la source
Dans le jeu d'instructions Intel x86, la construction d'une boucle pour décompter jusqu'à zéro peut généralement être effectuée avec moins d'instructions qu'une boucle comptant jusqu'à une condition de sortie différente de zéro. Plus précisément, le registre ECX est traditionnellement utilisé comme compteur de boucle dans x86 asm, et le jeu d'instructions Intel a une instruction de saut jcxz spéciale qui teste le registre ECX pour zéro et saute en fonction du résultat du test.
Cependant, la différence de performances sera négligeable à moins que votre boucle ne soit déjà très sensible au nombre de cycles d'horloge. Le compte à rebours jusqu'à zéro peut réduire de 4 à 5 cycles d'horloge à chaque itération de la boucle par rapport au compte à rebours, donc c'est vraiment plus une nouveauté qu'une technique utile.
De plus, un bon compilateur d'optimisation de nos jours devrait être capable de convertir votre code source de boucle de décompte en code machine de décompte à zéro (selon la façon dont vous utilisez la variable d'index de boucle), donc il n'y a vraiment aucune raison d'écrire vos boucles dans étranges façons de presser un cycle ou deux ici et là.
la source
Oui..!!
Compter de N à 0 est légèrement plus rapide que compter de 0 à N dans le sens de la façon dont le matériel gérera la comparaison.
Notez la comparaison dans chaque boucle
La plupart des processeurs ont une comparaison avec zéro instruction ... donc le premier sera traduit en code machine comme:
Mais le second a besoin de charger la mémoire de forme N à chaque fois
Ce n'est donc pas à cause du compte à rebours ou à la hausse ... mais à cause de la façon dont votre code sera traduit en code machine.
Donc, compter de 10 à 100 équivaut à compter de 100 à 10
Mais compter de i = 100 à 0 est plus rapide que de i = 0 à 100 - dans la plupart des cas
Et compter de i = N à 0 est plus rapide que de i = 0 à N
la source
En C à psudo-assemblage:
se transforme en
tandis que:
se transforme en
Notez l'absence de comparaison dans le deuxième psudo-assembly. Sur de nombreuses architectures, il existe des indicateurs définis par des opérations arithmatiques (ajouter, soustraire, multiplier, diviser, incrémenter, décrémenter) que vous pouvez utiliser pour les sauts. Ceux-ci vous donnent souvent ce qui est essentiellement une comparaison gratuite du résultat de l'opération avec 0. En fait sur de nombreuses architectures
est sémantiquement identique à
De plus, la comparaison avec un 10 dans mon exemple pourrait entraîner un code pire. 10 peuvent devoir vivre dans un registre, donc s'ils sont rares, cela coûte et peut entraîner un code supplémentaire pour déplacer les choses ou recharger le 10 à chaque fois dans la boucle.
Les compilateurs peuvent parfois réorganiser le code pour en tirer parti, mais c'est souvent difficile car ils ne peuvent souvent pas être sûrs que l'inversion de la direction dans la boucle est sémantiquement équivalente.
la source
i
n'est pas utilisé dans la boucle, vous pouvez évidemment le retourner, n'est-ce pas?Compte à rebours plus rapidement dans ce cas:
car
someObject.getAllObjects.size()
s'exécute une fois au début.Bien sûr, un comportement similaire peut être obtenu en appelant
size()
hors de la boucle, comme Peter l'a mentionné:la source
exec
.Peut être. Mais bien plus de 99% du temps, cela n'aura pas d'importance, vous devriez donc utiliser le test le plus `` sensé '' pour terminer la boucle, et par sens, je veux dire qu'il faut le moins de réflexion d'un lecteur pour comprendre ce que fait la boucle (y compris ce qui la fait s'arrêter). Faites en sorte que votre code corresponde au modèle mental (ou documenté) de ce que fait le code.
Si la boucle fonctionne dans un tableau (ou une liste, ou autre), un compteur incrémentiel correspondra souvent mieux à la façon dont le lecteur pourrait penser à ce que fait la boucle - codez votre boucle de cette façon.
Mais si vous travaillez dans un conteneur contenant des
N
articles et que vous les supprimez au fur et à mesure, il peut être plus logique de réduire le compteur.Un peu plus de détails sur le «peut-être» dans la réponse:
Il est vrai que sur la plupart des architectures, tester un calcul aboutissant à zéro (ou passant de zéro à négatif) ne nécessite aucune instruction de test explicite - le résultat peut être vérifié directement. Si vous souhaitez tester si un calcul aboutit à un autre nombre, le flux d'instructions devra généralement avoir une instruction explicite pour tester cette valeur. Cependant, en particulier avec les processeurs modernes, ce test ajoutera généralement moins de temps supplémentaire au niveau de bruit à une construction en boucle. Surtout si cette boucle effectue des E / S.
D'un autre côté, si vous comptez à rebours à partir de zéro et utilisez le compteur comme un index de tableau, par exemple, vous pourriez trouver le code fonctionnant contre l'architecture mémoire du système - les lectures de mémoire amèneront souvent un cache à `` regarder en avant '' plusieurs emplacements de mémoire au-delà de celui actuel en prévision d'une lecture séquentielle. Si vous travaillez à rebours dans la mémoire, le système de mise en cache peut ne pas anticiper les lectures d'un emplacement mémoire à une adresse mémoire inférieure. Dans ce cas, il est possible qu'une boucle «en arrière» puisse nuire aux performances. Cependant, je coderais probablement la boucle de cette façon (tant que les performances ne sont pas devenues un problème) car l'exactitude est primordiale, et faire correspondre le code à un modèle est un excellent moyen de garantir l'exactitude. Un code incorrect est aussi non optimisé que possible.
J'aurais donc tendance à oublier les conseils du professeur (bien sûr, pas sur son test cependant - vous devriez toujours être pragmatique en ce qui concerne la classe), à moins et jusqu'à ce que la performance du code ait vraiment d'importance.
la source
Sur certains processeurs plus anciens, il y a / existait des instructions comme
DJNZ
== "décrémenter et sauter sinon zéro". Cela permettait des boucles efficaces où vous chargiez une valeur de comptage initiale dans un registre et vous pouviez ensuite gérer efficacement une boucle de décrémentation avec une instruction. Nous parlons cependant des ISA des années 1980 - votre professeur est sérieusement déconnecté s'il pense que cette "règle de base" s'applique toujours aux processeurs modernes.la source
Bob,
Pas avant d'avoir fait des microoptimisations, à quel point vous aurez le manuel de votre CPU à portée de main. De plus, si vous faisiez ce genre de chose, vous n'auriez probablement pas besoin de poser cette question de toute façon. :-) Mais, votre professeur ne souscrit évidemment pas à cette idée ...
Il y a 4 choses à considérer dans votre exemple de boucle:
La comparaison est (comme d'autres l'ont indiqué) pertinente pour des architectures de processeur particulières . Il existe plus de types de processeurs que ceux qui exécutent Windows. En particulier, il peut y avoir une instruction qui simplifie et accélère les comparaisons avec 0.
Dans certains cas, il est plus rapide d'ajuster vers le haut ou vers le bas. En général, un bon compilateur le comprendra et refera la boucle s'il le peut. Cependant, tous les compilateurs ne sont pas bons.
Vous accédez à un appel système avec putchar. C'est extrêmement lent. De plus, vous effectuez un rendu sur l'écran (indirectement). C'est encore plus lent. Pensez à un rapport de 1000: 1 ou plus. Dans cette situation, le corps de boucle dépasse totalement et totalement le coût de l'ajustement / comparaison de la boucle.
Une disposition du cache et de la mémoire peut avoir un effet important sur les performances. Dans cette situation, cela n'a pas d'importance. Cependant, si vous accédiez à un tableau et que vous aviez besoin de performances optimales, il vous incomberait d'étudier comment votre compilateur et votre processeur ont organisé les accès à la mémoire et d'ajuster votre logiciel pour en tirer le meilleur parti. L'exemple de stock est celui donné en relation avec la multiplication matricielle.
la source
Ce qui compte beaucoup plus que d'augmenter ou de réduire votre compteur, c'est de savoir si vous augmentez ou diminuez la mémoire. La plupart des caches sont optimisés pour augmenter la mémoire, et non pour la perte de mémoire. Étant donné que le temps d'accès à la mémoire est le goulot d'étranglement auquel la plupart des programmes sont confrontés aujourd'hui, cela signifie que modifier votre programme afin d'augmenter la mémoire peut entraîner une augmentation des performances même si cela nécessite de comparer votre compteur à une valeur non nulle. Dans certains de mes programmes, j'ai constaté une amélioration significative des performances en modifiant mon code pour augmenter la mémoire au lieu de la réduire.
Sceptique? Il suffit d'écrire un programme pour chronométrer les boucles de mémoire. Voici le résultat que j'ai obtenu:
(où "mus" signifie microsecondes) de l'exécution de ce programme:
Les deux
sum_abs_up
etsum_abs_down
faire la même chose (somme le vecteur des nombres) et sont cadencés de la même manière avec la seule différence étant quesum_abs_up
monte la mémoire toutsum_abs_down
descend mémoire. Je passe mêmevec
par référence pour que les deux fonctions accèdent aux mêmes emplacements mémoire. Néanmoins,sum_abs_up
est toujours plus rapide quesum_abs_down
. Exécutez-le vous-même (je l'ai compilé avec g ++ -O3).Il est important de noter à quel point la boucle que je chronomètre est serrée. Si le corps d'une boucle est volumineux, le fait que son itérateur augmente ou diminue la mémoire n'a pas d'importance car le temps qu'il faut pour exécuter le corps de la boucle dominera probablement complètement. En outre, il est important de mentionner qu'avec certaines boucles rares, la réduction de la mémoire est parfois plus rapide que la remontée. Mais même avec de telles boucles, il n'a jamais été le cas que monter la mémoire était toujours plus lent que descendre (contrairement aux boucles de petite taille qui remontent la mémoire, pour lesquelles le contraire est souvent vrai; en fait, pour une petite poignée de boucles, je '' ve chronométré, l'augmentation des performances en augmentant la mémoire était de 40 +%).
Le point est, en règle générale, si vous avez l'option, si le corps de la boucle est petit, et s'il y a peu de différence entre faire remonter la mémoire de votre boucle au lieu de la réduire, alors vous devriez augmenter la mémoire.
FYI
vec_original
est là pour l'expérimentation, pour faciliter le changementsum_abs_up
etsum_abs_down
d'une manière qui les modifievec
tout en ne permettant pas à ces changements d'affecter les horaires futurs. Je recommande fortement de jouer avecsum_abs_up
etsum_abs_down
et de chronométrer les résultats.la source
quelle que soit la direction, utilisez toujours la forme du préfixe (++ i au lieu de i ++)!
ou
Explication: http://www.eskimo.com/~scs/cclass/notes/sx7b.html
De plus, vous pouvez écrire
Mais je m'attendrais à ce que les compilateurs modernes soient capables de faire exactement ces optimisations.
la source
C'est une question intéressante, mais en pratique, je ne pense pas que ce soit important et ne rend pas une boucle meilleure que l'autre.
Selon cette page wikipedia: Leap second , "... le jour solaire devient 1,7 ms plus long chaque siècle en raison principalement du frottement des marées." Mais si vous comptez les jours jusqu'à votre anniversaire, vous souciez-vous vraiment de cette petite différence de temps?
Il est plus important que le code source soit facile à lire et à comprendre. Ces deux boucles sont un bon exemple de l'importance de la lisibilité: elles ne bouclent pas le même nombre de fois.
Je parierais que la plupart des programmeurs lisent (i = 0; i <N; i ++) et comprennent immédiatement que cela boucle N fois. Une boucle de (i = 1; i <= N; i ++), pour moi en tout cas, est un peu moins claire, et avec (i = N; i> 0; i--) je dois y réfléchir un instant . Il est préférable que l'intention du code entre directement dans le cerveau sans aucune réflexion requise.
la source
Curieusement, il semble qu'il y ait une différence. Au moins, en PHP. Pensez à suivre le benchmark:
Les résultats sont intéressants:
Si quelqu'un sait pourquoi, ce serait bien de savoir :)
EDIT : Les résultats sont les mêmes même si vous commencez à compter non pas à partir de 0, mais d'une autre valeur arbitraire. Il n'y a donc probablement pas que la comparaison à zéro qui fait la différence?
la source
Cela peut être plus rapide.
Sur le processeur NIOS II avec lequel je travaille actuellement, la boucle for traditionnelle
produit l'assemblage:
Si on compte à rebours
nous obtenons un assemblage qui nécessite 2 instructions de moins.
Si nous avons des boucles imbriquées, où la boucle interne est beaucoup exécutée, nous pouvons avoir une différence mesurable:
Si la boucle interne est écrite comme ci-dessus, le temps d'exécution est: 0,12199999999999999734 secondes. Si la boucle interne est écrite de manière traditionnelle, le temps d'exécution est: 0,17199999999999998623 secondes. Ainsi, le compte à rebours de la boucle est environ 30% plus rapide.
Mais: ce test a été fait avec toutes les optimisations GCC désactivées. Si nous les activons, le compilateur est en fait plus intelligent que cette optimisation pratique et conserve même la valeur dans un registre pendant toute la boucle et nous obtiendrions un assemblage comme
Dans cet exemple particulier, le compilateur remarque même que la variable a sera toujours 1 après l'exécution de la boucle et saute toutes les boucles.
Cependant, j'ai constaté que parfois, si le corps de la boucle est suffisamment complexe, le compilateur n'est pas capable de faire cette optimisation, donc le moyen le plus sûr d'obtenir toujours une exécution rapide de la boucle est d'écrire:
Bien sûr, cela ne fonctionne que si cela n'a pas d'importance que la boucle soit exécutée à l'envers et comme Betamoo l'a dit, seulement si vous comptez à rebours jusqu'à zéro.
la source
Ce que votre professeur a dit était une déclaration oblique sans beaucoup de clarification. Ce n'est PAS que la décrémentation soit plus rapide que l'incrémentation, mais vous pouvez créer une boucle beaucoup plus rapide avec décrémentation qu'avec incrémentation.
Sans en parler longuement, sans avoir besoin d'utiliser un compteur de boucles, etc. - ce qui compte ci-dessous, c'est juste la vitesse et le nombre de boucles (non nul).
Voici comment la plupart des gens implémentent la boucle avec 10 itérations:
Dans 99% des cas, c'est tout ce dont on peut avoir besoin, mais avec PHP, PYTHON, JavaScript, il y a tout le monde des logiciels critiques (généralement embarqués, OS, jeux, etc.) où les ticks CPU comptent vraiment, alors regardez brièvement le code d'assemblage de:
après compilation (sans optimisation), la version compilée peut ressembler à ceci (VS2015):
La boucle entière est de 8 instructions (26 octets). Il contient en fait 6 instructions (17 octets) avec 2 branches. Oui, je sais que cela peut être mieux fait (c'est juste un exemple).
Maintenant, considérez cette construction fréquente que vous trouverez souvent écrite par un développeur embarqué:
Il itère également 10 fois (oui, je sais que la valeur i est différente de celle indiquée pour la boucle, mais nous nous soucions du nombre d'itérations ici). Cela peut être compilé dans ceci:
5 instructions (18 octets) et une seule branche. En fait, il y a 4 instructions dans la boucle (11 octets).
La meilleure chose est que certains processeurs (compatibles x86 / x64 inclus) ont des instructions qui peuvent décrémenter un registre, comparer ultérieurement le résultat à zéro et effectuer une branche si le résultat est différent de zéro. Pratiquement TOUS les processeurs PC implémentent cette instruction. En l'utilisant, la boucle n'est en fait qu'une instruction (oui un) de 2 octets:
Dois-je expliquer ce qui est le plus rapide?
Maintenant, même si un processeur particulier n'implémente pas l'instruction ci-dessus, tout ce dont il a besoin pour l'émuler est un décrément suivi d'un saut conditionnel si le résultat de l'instruction précédente s'avère être zéro.
Donc, indépendamment de certains cas que vous pouvez signaler en commentaire, pourquoi je me trompe, etc. J'insiste sur le fait qu'il est avantageux de faire une boucle vers le bas si vous savez comment, pourquoi et quand.
PS. Oui, je sais que le compilateur sage (avec le niveau d'optimisation approprié) réécrira pour la boucle (avec le compteur de boucle ascendant) en do..tandis que l'équivalent pour les itérations de boucle constante ... (ou le déroulera) ...
la source
Non, ce n'est pas vraiment vrai. Une situation où cela pourrait être plus rapide est lorsque vous appelleriez autrement une fonction pour vérifier les limites à chaque itération d'une boucle.
Mais s'il est moins clair de le faire de cette façon, cela n'en vaut pas la peine. Dans les langues modernes, vous devez utiliser une boucle foreach lorsque cela est possible, de toute façon. Vous mentionnez spécifiquement le cas où vous devriez utiliser une boucle foreach - lorsque vous n'avez pas besoin de l'index.
la source
for(int i=0, siz=myCollection.size(); i<siz; i++)
.Le fait est que lors du décompte, vous n'avez pas besoin de vérifier
i >= 0
séparément pour décrémenteri
. Observer:La comparaison et la décrémentation
i
peuvent être effectuées dans une seule expression.Voir d'autres réponses pour savoir pourquoi cela se résume à moins d'instructions x86.
Quant à savoir si cela fait une différence significative dans votre application, eh bien je suppose que cela dépend du nombre de boucles que vous avez et de la profondeur de leur imbrication. Mais pour moi, c'est tout aussi lisible de le faire de cette façon, alors je le fais quand même.
la source
Maintenant, je pense que vous avez eu assez de conférences d'assemblée :) Je voudrais vous présenter une autre raison pour une approche descendante.
La raison d'aller du haut est très simple. Dans le corps de la boucle, vous pouvez accidentellement modifier la limite, ce qui peut aboutir à un comportement incorrect ou même à une boucle sans fin.
Regardez cette petite partie du code Java (le langage n'a pas d'importance je suppose pour cette raison):
Donc, mon point est que vous devriez envisager de préférer aller du haut vers le bas ou avoir une constante comme limite.
la source
for (int i=0; i < 999; i++) {
.for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Au niveau de l'assembleur, une boucle qui compte à rebours jusqu'à zéro est généralement légèrement plus rapide qu'une boucle qui compte jusqu'à une valeur donnée. Si le résultat d'un calcul est égal à zéro, la plupart des processeurs définiront un indicateur zéro. Si en soustrayant un, le calcul passe autour de zéro, cela changera normalement l'indicateur de report (sur certains processeurs, il le mettra sur d'autres, il l'effacera), de sorte que la comparaison avec zéro est essentiellement gratuite.
Ceci est encore plus vrai lorsque le nombre d'itérations n'est pas une constante mais une variable.
Dans des cas triviaux, le compilateur peut être en mesure d'optimiser automatiquement la direction de comptage d'une boucle, mais dans des cas plus complexes, il se peut que le programmeur sache que la direction de la boucle n'est pas pertinente pour le comportement global, mais le compilateur ne peut pas le prouver.
la source