Les boucles sont-elles vraiment plus rapides en sens inverse?

268

J'ai entendu cela plusieurs fois. Les boucles JavaScript sont-elles vraiment plus rapides lors du comptage à rebours? Si oui, pourquoi? J'ai vu quelques exemples de suite de tests montrant que les boucles inversées sont plus rapides, mais je ne trouve aucune explication sur pourquoi!

Je suppose que c'est parce que la boucle n'a plus à évaluer une propriété à chaque fois qu'elle vérifie si elle est terminée et qu'elle vérifie simplement la valeur numérique finale.

C'est à dire

for (var i = count - 1; i >= 0; i--)
{
  // count is only evaluated once and then the comparison is always on 0.
}
djdd87
la source
6
hehe. cela prendra indéfiniment. essayez i--
StampedeXV
5
La forboucle en arrière est plus rapide car la variable de contrôle de boucle de limite supérieure (hehe, borne inférieure) n'a pas besoin d'être définie ou extraite d'un objet; c'est un zéro constant.
Josh Stodola
88
Il n'y a pas vraiment de différence . Les constructions de boucles natives seront toujours très rapides . Ne vous inquiétez pas de leurs performances.
James Allardice
24
@Afshin: Pour des questions comme celle-ci, veuillez nous montrer les articles auxquels vous faites référence.
Courses de légèreté en orbite
22
Il existe une différence principalement importante pour les appareils très bas de gamme et alimentés par batterie. Les différences sont qu'avec i-- vous vous comparez à 0 pour la fin de la boucle, tandis qu'avec i ++ vous comparez avec un nombre> 0. Je crois que la différence de performance était quelque chose comme 20 nanosecondes (quelque chose comme cmp ax, 0 vs cmp ax , bx) - ce qui n'est rien, mais si vous bouclez des milliers de fois par seconde, pourquoi ne pas avoir un gain de 20 nanosecondes pour chaque :)
luben

Réponses:

903

Ce n'est pas que i--c'est plus rapide que i++. En fait, ils sont tous deux aussi rapides.

Ce qui prend du temps dans les boucles ascendantes, c'est d'évaluer, pour chacune i, la taille de votre tableau. Dans cette boucle:

for(var i = array.length; i--;)

Vous n'évaluez .lengthqu'une seule fois, lorsque vous déclarez i, alors que pour cette boucle

for(var i = 1; i <= array.length; i++)

vous évaluez .lengthchaque fois que vous augmentez i, lorsque vous vérifiez sii <= array.length .

Dans la plupart des cas, vous ne devriez même pas vous soucier de ce type d'optimisation .

alestanis
la source
23
vaut-il la peine d'introduire une variable pour array.length et de l'utiliser dans la tête de la boucle for?
ragatskynet
39
@ragatskynet: Non, à moins que vous n'établissiez une référence et que vous vouliez faire valoir un point.
Jon
29
@ragatskynet Cela dépend: sera-t-il plus rapide d'évaluer .lengthun certain nombre de fois ou de déclarer et définir une nouvelle variable? Dans la plupart des cas, il s'agit d'une optimisation prématurée (et erronée), sauf si votre lengthévaluation est très coûteuse.
alestanis
10
@ Dr.Dredel: Ce n'est pas la comparaison - c'est l'évaluation. 0est plus rapide à évaluer que array.length. Eh bien, soi-disant.
Courses de légèreté en orbite
22
Il convient de mentionner que nous parlons de langages interprétés tels que Ruby, Python. Les langages compilés, par exemple Java, ont des optimisations au niveau du compilateur qui "atténueront" ces différences au point qu'il importe peu que ce .lengthsoit en déclaration for loopou non.
Haris Krajina
198

Ce mec a comparé beaucoup de boucles en javascript, dans beaucoup de navigateurs. Il dispose également d'une suite de tests pour que vous puissiez les exécuter vous-même.

Dans tous les cas (sauf si j'en ai manqué un dans ma lecture), la boucle la plus rapide était:

var i = arr.length; //or 10
while(i--)
{
  //...
}
Tom Ritter
la source
Nice :) Mais il n'y a pas de boucle "for" testée en arrière ... Mais les boucles for mentionnées par peirix ou searlea devraient être à peu près les mêmes que la boucle "while" avec "i--" comme condition. Et c'était la boucle la plus rapide testée.
SvenFinke
11
Intéressant, mais je me demande si la pré-décrémentation serait encore plus rapide. Puisqu'il n'aura pas à stocker la valeur intermédiaire de i.
tvanfosson
Si je me souviens bien, mon prof de cours de matériel informatique a dit que ce test pour 0 ou non 0 est le "calcul" le plus rapide possible. Pendant que (i--) le test est toujours un test pour 0. C'est peut-être pourquoi c'est le plus rapide?
StampedeXV
3
@tvanfosson si vous pré-décrémentez --ialors vous devez utiliser var current = arr[i-1];à l'intérieur de la boucle ou il sera éteint par un ...
DaveAlger
2
J'ai entendu dire que i-- peut être plus rapide que --i parce que dans le deuxième cas, le processeur doit décrémenter puis tester par rapport à la nouvelle valeur (il y a une dépendance de données entre les instructions), tandis que dans le premier cas, le processeur peut tester la valeur existante et décrémenter la valeur un peu plus tard. Je ne sais pas si cela s'applique à JavaScript ou seulement au code C de très bas niveau.
marcus
128

J'essaie de donner une vue d'ensemble avec cette réponse.

Les réflexions suivantes entre parenthèses étaient ma conviction jusqu'à ce que je viens de tester le problème récemment:

[[En termes de langages de bas niveau comme C / C ++ , le code est compilé de sorte que le processeur dispose d'une commande de saut conditionnel spéciale lorsqu'une variable est nulle (ou non nulle).
De plus, si vous vous souciez de cette optimisation, vous pouvez aller à la ++iplace de i++, car il ++is'agit d'une commande à processeur unique alors que cela i++signifiej=i+1, i=j .]]

Des boucles vraiment rapides peuvent être faites en les déroulant:

for(i=800000;i>0;--i)
    do_it(i);

Cela peut être beaucoup plus lent que

for(i=800000;i>0;i-=8)
{
    do_it(i); do_it(i-1); do_it(i-2); ... do_it(i-7);
}

mais les raisons peuvent être assez compliquées (juste pour mentionner, il y a les problèmes de prétraitement des commandes du processeur et de gestion du cache dans le jeu).

En termes de langages de haut niveau , comme JavaScript comme vous l'avez demandé, vous pouvez optimiser les choses si vous comptez sur des bibliothèques, des fonctions intégrées pour la boucle. Laissez-les décider de la meilleure façon de procéder.

Par conséquent, en JavaScript, je suggère d'utiliser quelque chose comme

array.forEach(function(i) {
    do_it(i);
});

Il est également moins sujet aux erreurs et les navigateurs ont la possibilité d'optimiser votre code.

[REMARQUE: non seulement les navigateurs, mais vous aussi avez un espace pour optimiser facilement, redéfinissez simplement la forEachfonction (en fonction du navigateur) afin qu'elle utilise la dernière supercherie! :) @AMK dit que dans des cas particuliers, il vaut mieux utiliser array.popou array.shift. Si vous faites cela, mettez-le derrière le rideau. Le plus exagéré est d'ajouter des options forEachpour sélectionner l'algorithme de bouclage.]

De plus, également pour les langages de bas niveau, la meilleure pratique consiste à utiliser une fonction de bibliothèque intelligente pour les opérations complexes en boucle si cela est possible.

Ces bibliothèques peuvent également mettre des choses (multithread) derrière votre dos et des programmeurs spécialisés les tiennent à jour.

J'ai fait un peu plus de contrôle et il s'avère qu'en C / C ++, même pour les opérations 5e9 = (50,000x100,000), il n'y a pas de différence entre monter et descendre si le test est effectué contre une constante comme @alestanis le dit. (Les résultats JsPerf sont parfois incohérents, mais en gros, ils disent la même chose: vous ne pouvez pas faire une grande différence.)
Il --ise trouve donc que c'est plutôt une chose "chic". Cela vous fait seulement ressembler à un meilleur programmeur. :)

D'un autre côté, pour se dérouler dans cette situation de 5e9, cela m'a ramené de 12 secondes à 2,5 secondes quand je suis passé de 10 secondes, et à 2,1 secondes quand je suis allé de 20 secondes. C'était sans optimisation, et l'optimisation a réduit les choses à peu de temps. :) (Le déroulement peut être fait à ma façon ci-dessus ou en utilisant i++, mais cela ne fait pas avancer les choses en JavaScript.)

Dans l'ensemble: conservez i--/ i++et ++i/ les i++différences dans les entretiens d'embauche, respectez array.forEachou d'autres fonctions de bibliothèque complexes lorsqu'elles sont disponibles. ;)

Barney
la source
18
Le mot clé est "peut être". Votre boucle déroulée peut également être plus lente que celle d'origine. Lors de l'optimisation, mesurez toujours afin de savoir exactement quel impact vos modifications ont eu.
2012 à 11h59
1
@jalf true en effet, +1. Différentes longueurs de déverrouillage (> = 1) sont différentes en termes d'efficacité. C'est pourquoi il est plus pratique de laisser ce travail aux bibliothèques si possible, sans oublier que les navigateurs fonctionnent sur des architectures différentes, il serait donc préférable qu'ils décident de la façon de le faire array.each(...). Je ne pense pas qu'ils essaieraient d'expérimenter avec des boucles de boucles simples.
Barney
1
@BarnabasSzabolcs La question concerne spécifiquement JavaScript, pas C ou d'autres langages. Dans JS il y a une différence, veuillez voir ma réponse ci-dessous. Bien que cela ne s'applique pas à la question, +1 bonne réponse!
AMK
1
Et voilà, vous +1obtenez un Great Answerbadge. Vraiment une excellente réponse. :)
Rohit Jain
1
APL / A ++ n'est pas non plus un langage. Les gens les utilisent pour exprimer qu'ils ne s'intéressent pas aux spécificités linguistiques, mais à quelque chose que ces langues partagent.
Barney
33

i-- est aussi rapide que i++

Ce code ci-dessous est aussi rapide que le vôtre, mais utilise une variable supplémentaire:

var up = Things.length;
for (var i = 0; i < up; i++) {
    Things[i]
};

La recommandation est de NE PAS évaluer la taille du tableau à chaque fois. Pour les grands tableaux, on peut voir la dégradation des performances.

sainiuc
la source
6
Vous vous trompez manifestement ici. Maintenant, le contrôle de boucle nécessite une variable interne supplémentaire (i et plus) et selon le moteur JavaScript, cela peut limiter le potentiel d'optimisation du code. Les JIT traduiront des boucles simples en opcodes machine directs, mais si les boucles ont trop de variables, JIT ne pourra pas optimiser aussi bien. Généralement, il est limité par l'architecture ou les registres cpu que JIT utilise. Initialiser une fois et descendre la solution la plus propre et c'est pourquoi il est recommandé partout.
Pavel P
La variable supplémentaire sera éliminée par l'optimiseur d'un interpréteur / compilateur commun. Le point est le «comparer à 0» - voir ma réponse pour plus d'explications.
H.-Dirk Schmitt
1
Mieux vaut mettre 'up' dans la boucle:for (var i=0, up=Things.length; i<up; i++) {}
thdoan
22

Depuis, vous êtes intéressé par le sujet, jetez un œil au billet de blog de Greg Reimer sur un benchmark de boucle JavaScript, Quel est le moyen le plus rapide de coder une boucle en JavaScript? :

J'ai construit une suite de tests d'analyse comparative des boucles pour différentes façons de coder les boucles en JavaScript. Il y en a déjà quelques-uns, mais je n'en ai trouvé aucun qui reconnaissait la différence entre les tableaux natifs et les collections HTML.

Vous pouvez également faire un test de performance sur une boucle en ouvrant https://blogs.oracle.com/greimer/resource/loop-test.html(ne fonctionne pas si JavaScript est bloqué dans le navigateur par, par exemple, NoScript ).

ÉDITER:

Un benchmark plus récent créé par Milan Adamovsky peut être exécuté au moment de l'exécution ici au pour différents navigateurs.

Pour un test dans Firefox 17.0 sur Mac OS X 10.6 j'ai obtenu la boucle suivante:

var i, result = 0;
for (i = steps - 1; i; i--) {
  result += i;
}

comme le plus rapide précédé de:

var result = 0;
for (var i = steps - 1; i >= 0; i--) {
  result += i;
}

Exemple de référence.

dreamcrash
la source
5
Ce poste a maintenant 4 ans. Compte tenu de la vitesse (ha!) À laquelle les moteurs JavaScript ont été mis à jour, la plupart des conseils sont probablement obsolètes - l'article ne mentionne même pas Chrome et son moteur JavaScript V8 brillant.
Yi Jiang
Oui, ce détail me manque, merci de l'avoir signalé. À ce moment-là, il n'y avait pas beaucoup de différence entre --i ou i ++ sur la boucle de.
dreamcrash
19

Ce n'est pas le --ou ++, c'est l'opération de comparaison. Avec, --vous pouvez utiliser une comparaison avec 0, tandis qu'avec, ++vous devez la comparer avec la longueur. Sur le processeur, la comparaison avec zéro est normalement disponible, tandis que la comparaison avec un entier fini nécessite une soustraction.

a++ < length

est en fait compilé comme

a++
test (a-length)

Cela prend donc plus de temps au processeur lors de la compilation.

Dr BDO Adams
la source
15

Réponse courte

Pour le code normal, en particulier dans un langage de haut niveau comme JavaScript , il n'y a pas de différence de performances dans i++et i--.

Le critère de performance est l'utilisation dans la forboucle et l' instruction compare .

Cela s'applique à tous les langages de haut niveau et est principalement indépendant de l'utilisation de JavaScript. L'explication est le code assembleur résultant en bout de ligne.

Explication détaillée

Une différence de performances peut se produire dans une boucle. L'arrière-plan est qu'au niveau du code assembleur , vous pouvez voir que a compare with 0n'est qu'une instruction qui n'a pas besoin d'un registre supplémentaire.

Cette comparaison est émise à chaque passage de la boucle et peut entraîner une amélioration mesurable des performances.

for(var i = array.length; i--; )

sera évalué en un pseudo-code comme celui-ci:

 i=array.length
 :LOOP_START
 decrement i
 if [ i = 0 ] goto :LOOP_END
 ... BODY_CODE
 :LOOP_END

Notez que 0 est un littéral, ou en d'autres termes, une valeur constante.

for(var i = 0 ; i < array.length; i++ )

sera évalué en un pseudo-code comme celui-ci (optimisation normale de l'interpréteur supposée):

 end=array.length
 i=0
 :LOOP_START
 if [ i < end ] goto :LOOP_END
 increment i
 ... BODY_CODE
 :LOOP_END

Notez que end est une variable qui a besoin d'un registre CPU . Cela peut invoquer un échange de registre supplémentaire dans le code et nécessite une instruction de comparaison plus coûteuse dans l' ifinstruction.

Juste mes 5 cents

Pour un langage de haut niveau, la lisibilité, qui facilite la maintenabilité, est plus importante en tant qu'amélioration mineure des performances.

Normalement, l' itération classique du début à la fin du tableau est meilleure.

L'itération plus rapide de la fin du tableau au début entraîne la séquence inversée potentiellement indésirable.

Post Scriptum

Comme demandé dans un commentaire: La différence de --iet i--réside dans l’évaluation desi avant ou après la décrémentation.

La meilleure explication est de l'essayer ;-) Voici un exemple Bash .

 % i=10; echo "$((--i)) --> $i"
 9 --> 9
 % i=10; echo "$((i--)) --> $i"
 10 --> 9
H.-Dirk Schmitt
la source
1
1+ Bonne explication. Juste une question un peu hors de portée, pourriez-vous expliquer la différence entre --iet i--aussi?
Afshin Mehrabani
14

J'ai vu la même recommandation dans Sublime Text 2.

Comme cela a déjà été dit, la principale amélioration n'est pas d'évaluer la longueur du tableau à chaque itération de la boucle for. C'est une technique d'optimisation bien connue et particulièrement efficace en JavaScript lorsque le tableau fait partie du document HTML (en faisant un forpour tous les liéléments).

Par exemple,

for (var i = 0; i < document.getElementsByTagName('li').length; i++)

est beaucoup plus lent que

for (var i = 0, len = document.getElementsByTagName('li').length; i < len; i++)

D'où je me tiens, la principale amélioration du formulaire dans votre question est le fait qu'il ne déclare pas de variable supplémentaire ( lendans mon exemple)

Mais si vous me demandez, le point ne concerne pas l' optimisation i++vs i--, mais le fait de ne pas avoir à évaluer la longueur du tableau à chaque itération (vous pouvez voir un test de référence sur jsperf ).

BBog
la source
1
Je dois m'opposer au mot calcul ici. Voir mon commentaire sur la réponse de Pavel . La spécification ECMA indique que la longueur du tableau n'est pas calculée lorsque vous vous y référez.
kojiro
L'évaluation serait-elle un meilleur choix? Fait intéressant btw, je ne savais pas que
BBog
La variable supplémentaire sera éliminée par l'optimiseur d'un interpréteur / compilateur commun. Il en va de même pour l'évaluation array.length. Le point est le «comparer à 0» - voir ma réponse pour plus d'explications.
H.-Dirk Schmitt
@ H.-DirkSchmitt, à part votre réponse de bon sens, pendant longtemps dans l'histoire de JavaScript, le compilateur n'a pas optimisé le coût de performance de la chaîne de recherche. AFAIK V8 a été le premier à essayer.
kojiro
@ H.-DirkSchmitt, comme l'a dit kojiro, c'est une astuce bien connue et bien établie. Même s'il n'est plus pertinent dans les navigateurs modernes, cela ne le rend toujours pas obsolète. De plus, le faire avec l'introduction d'une nouvelle variable de longueur ou avec l'astuce de la question OP, c'est toujours la meilleure façon, imo. C'est juste une chose intelligente à faire et une bonne pratique, je ne pense pas que cela ait quelque chose à voir avec le fait que le compilateur a été optimisé pour prendre en charge quelque chose souvent mal fait dans JS
BBog
13

Je ne pense pas qu'il soit logique de dire que i--c'est plus rapide quei++ JavaScript.

Tout d'abord , cela dépend totalement de l'implémentation du moteur JavaScript.

Deuxièmement , à condition que les constructions les plus simples soient JIT et traduites en instructions natives, alors i++vs i--dépendra totalement du CPU qui l'exécute. Autrement dit, sur les ARM (téléphones mobiles), il est plus rapide de descendre à 0 car la décrémentation et la comparaison avec zéro sont exécutées en une seule instruction.

Vous pensiez probablement que l'un était plus gaspillé que l'autre parce que la manière suggérée est

for(var i = array.length; i--; )

mais la manière suggérée n'est pas parce que l'un est plus rapide que l'autre, mais simplement parce que si vous écrivez

for(var i = 0; i < array.length; i++)

puis à chaque itération array.length devait être évaluée (un moteur JavaScript plus intelligent pourrait peut-être comprendre que la boucle ne changerait pas la longueur du tableau). Même si cela ressemble à une simple déclaration, c'est en fait une fonction qui est appelée sous le capot par le moteur JavaScript.

L'autre raison, qui i--pourrait être considérée comme "plus rapide", est que le moteur JavaScript n'a besoin d'allouer qu'une seule variable interne pour contrôler la boucle (variable à la var i). Si vous comparez à array.length ou à une autre variable, il doit y avoir plus d'une variable interne pour contrôler la boucle, et le nombre de variables internes est un atout limité d'un moteur JavaScript. Moins les variables sont utilisées dans une boucle, plus JIT a de chances d'optimiser. Voilà pourquoi i--pourrait être considéré comme plus rapide ...

Pavel P
la source
3
Cela vaut probablement la peine d'être formulé avec soin sur la façon dont array.lengthest évalué. La longueur n'est pas calculée lorsque vous vous y référez. (C'est juste une propriété qui est définie chaque fois qu'un index de tableau est créé ou modifié ). Lorsqu'il y a un coût supplémentaire, c'est parce que le moteur JS n'a pas optimisé la chaîne de recherche pour ce nom.
kojiro
Eh bien, je ne suis pas sûr de ce que dit la spécification Ecma, mais connaître certains éléments internes de différents moteurs JavaScript n'est pas simple getLength(){return m_length; }car il y a un peu d'entretien interne impliqué. Mais si vous essayez de penser en arrière: ce serait assez ingénieux d'écrire une implémentation de tableau où la longueur devrait être réellement calculée :)
Pavel P
la spécification ECMA exige que la propriété de longueur soit déjà calculée. Le lengthdoit être mis à jour immédiatement chaque fois qu'une propriété-qui-est-un-tableau-index est ajoutée ou modifiée.
kojiro
Ce que j'essaie de dire, c'est qu'il est assez difficile de violer la spécification si vous essayez d'y penser.
Pavel P
1
x86 est comme ARM à cet égard. dec/jnzpar rapport à inc eax / cmp eax, edx / jne.
Peter Cordes
13

Puisqu'aucune des autres réponses ne semble répondre à votre question spécifique (plus de la moitié d'entre elles montrent des exemples C et discutent des langages de niveau inférieur, votre question est pour JavaScript) J'ai décidé d'écrire la mienne.

Alors, c'est parti:

Réponse simple: i-- est généralement plus rapide car il n'a pas à exécuter une comparaison à 0 à chaque exécution, les résultats des tests sur différentes méthodes sont ci-dessous:

Résultats des tests: Comme "prouvé" par ce jsPerf, arr.pop()c'est en fait la boucle la plus rapide de loin. Mais, en se concentrant sur --i, i--, i++et++i que vous avez demandé à votre question, voici jsPerf (ils sont de multiples jsPerf de, s'il vous plaît voir les sources ci - dessous) Les résultats résumés:

--iet i--sont les mêmes dans Firefox tandis que i--c'est plus rapide dans Chrome.

Dans Chrome, une boucle for de base ( for (var i = 0; i < arr.length; i++)) est plus rapide i--et--i tandis que dans Firefox, elle est plus lente.

Dans Chrome et Firefox, un cache arr.length est beaucoup plus rapide avec Chrome en avance d'environ 170 000 opérations / s.

Sans différence significative, ++iest plus rapide quei++ dans la plupart des navigateurs, AFAIK, ce n'est jamais l'inverse dans n'importe quel navigateur.

Résumé plus court: arr.pop() c'est de loin la boucle la plus rapide; pour les boucles spécifiquement mentionnées,i-- est la boucle la plus rapide.

Sources: http://jsperf.com/fastest-array-loops-in-javascript/15 , http://jsperf.com/ipp-vs-ppi-2

J'espère que cela répond à votre question.

AMK
la source
1
Il semble que votre poptest ne semble que très rapide car il réduit la taille du tableau à 0 pour la majorité de la boucle - pour autant que je sache. Cependant, pour m'assurer que les choses sont justes, j'ai conçu ce jsperf pour créer le tableau de la même manière à chaque test - ce qui semble se révéler .shift()être le gagnant pour mes quelques navigateurs - pas ce à quoi je m'attendais cependant :) jsperf.com / comparer-différents-types-de boucle
Pebbl
A voté pour être le seul à mentionner ++i: D
jaggedsoft
12

Cela dépend du placement de votre baie dans la mémoire et du taux de succès des pages de mémoire lorsque vous accédez à cette baie.

Dans certains cas, l'accès aux membres du tableau dans l'ordre des colonnes est plus rapide que l'ordre des lignes en raison de l'augmentation du taux d'accès.

Akbar Bayati
la source
1
Si seulement l'OP avait demandé pourquoi le fait de traverser la même matrice dans des ordres différents peut prendre des quantités de temps différentes.
dcow
1
Considérant que dans les systèmes d'exploitation que leur gestion de la mémoire est basée sur la pagination, lorsqu'un processus a besoin de données qui ne sont pas dans des pages mises en cache, il se produit un défaut de page dans le système d'exploitation et il doit amener la page cible dans le cache du processeur et la remplacer par une autre page, par conséquent, entraîne une surcharge de traitement qui prend plus de temps que lorsque la page cible est dans le cache du processeur. Supposons que nous ayons défini un grand tableau que chaque ligne soit plus grande que la taille du système d'exploitation de la page et que nous y accédions dans l'ordre des lignes, dans ce cas, le taux de défaillance des pages augmente et le résultat est plus lent que l'accès par ordre des colonnes à ce tableau.
Akbar Bayati
Les défauts de page ne sont pas la même chose que les échecs de cache. Vous ne bloquez la page que si votre mémoire a été paginée sur le disque. L'accès aux tableaux multidimensionnels dans l'ordre séquentiel de la façon dont ils sont stockés en mémoire est plus rapide en raison de la localisation du cache (en utilisant tous les octets d'une ligne de cache lorsqu'elle est récupérée), pas en raison de défauts de page. (à moins que votre poste de travail ne soit trop grand pour l'ordinateur que vous utilisez.)
Peter Cordes
10

La dernière fois que je m'en suis inquiété, c'était lors de l'écriture de l' assemblage 6502 (8 bits, oui!). Le gros gain est que la plupart des opérations arithmétiques (en particulier les décrémentations) ont mis à jour un ensemble de drapeaux, dont l'un était Zl'indicateur «atteint zéro».

Donc, à la fin de la boucle, vous venez de faire deux instructions: DEC(décrémenter) et JNZ(sauter sinon zéro), aucune comparaison nécessaire!

Javier
la source
Dans le cas de JavaScript, cela ne s'applique évidemment pas (car il fonctionne sur des processeurs qui n'ont pas de tels codes opérationnels). La vraie raison derrière le i--vs i++est probablement qu'avec le premier, vous n'introduisez pas de variables de contrôle supplémentaires dans la portée de la boucle. Voir ma réponse ci-dessous ...
Pavel P
1
à droite, cela ne s'applique vraiment pas; mais c'est un style C très courant, et il semble plus propre à ceux d'entre nous qui s'y sont habitués. :-)
Javier
x86 et ARM sont tous deux comme 6502 à cet égard. dec/jnzau lieu du inc/cmp/jnecas x86. Vous ne verrez pas une boucle vide s'exécuter plus rapidement (les deux satureront le débit de la branche), mais le décompte réduit légèrement la surcharge de la boucle. Les pré-récupérateurs Intel HW actuels ne sont pas gênés non plus par les modèles d'accès à la mémoire qui vont dans l'ordre décroissant. Je pense que les processeurs plus anciens pourraient suivre comme 4 flux en arrière et 6 ou 10 flux en avant, IIRC.
Peter Cordes
7

Cela peut s'expliquer par le fait que JavaScript (et toutes les langues) est éventuellement transformé en opcodes pour s'exécuter sur le CPU. Les processeurs ont toujours une seule instruction pour comparer avec zéro, ce qui est sacrément rapide.

Soit dit en passant, si vous pouvez garantir countest toujours >= 0, vous pouvez simplifier pour:

for (var i = count; i--;)
{
  // whatever
}
searlea
la source
1
Un code source plus court ne signifie pas nécessairement qu'il sera plus rapide. L'avez-vous mesuré?
Greg Hewgill
Oups, j'ai raté celui-ci. Clou sur la tête il y a chap.
Robert
1
Je voudrais voir les sources d'assemblage où la comparaison avec 0 est différente. Cela fait quelques années, mais à un moment donné, j'ai fait une tonne de codage d'assemblage et je ne peux pas pour la vie de penser à un moyen de comparer / tester contre 0 d'une manière qui ne peut pas être aussi rapide pour tout autre entier. Pourtant, ce que vous dites est vrai. Frustré que je ne comprenne pas pourquoi!
Brian Knoblauch
7
@Brian Knoblauch: Si vous utilisez une instruction telle que "dec eax" (code x86), cette instruction définit automatiquement l'indicateur Z (zéro) que vous pouvez tester immédiatement sans avoir à utiliser une autre instruction de comparaison entre les deux.
Greg Hewgill
1
Lorsque j'interprète Javascript, je doute que les opcodes soient le goulot d'étranglement. Il est plus probable que moins de jetons signifie que l'interpréteur peut traiter le code source plus rapidement.
Todd Owen
6

Cela ne dépend pas du signe --ou ++, mais cela dépend des conditions que vous appliquez dans la boucle.

Par exemple: votre boucle est plus rapide si la variable a une valeur statique que si votre boucle vérifie les conditions à chaque fois, comme la longueur d'un tableau ou d'autres conditions.

Mais ne vous inquiétez pas de cette optimisation, car cette fois son effet est mesuré en nanosecondes.

Arvind Kanjariya
la source
plusieurs boucles de nanosecondes peuvent devenir des secondes ... ce n'est jamais une mauvaise idée d'optimiser quand vous en avez le temps
Buksy
6

for(var i = array.length; i--; )n'est pas beaucoup plus rapide. Mais lorsque vous remplacez array.lengthpar super_puper_function(), cela peut être beaucoup plus rapide (car il est appelé à chaque itération). Voilà la différence.

Si vous comptez le changer en 2014, vous n'avez pas besoin de penser à l'optimisation. Si vous voulez le changer avec "Rechercher et remplacer", vous n'avez pas besoin de penser à l'optimisation. Si vous n'avez pas le temps, vous n'avez pas besoin de penser à l'optimisation. Mais maintenant, vous avez le temps d'y penser.

PS: i--n'est pas plus rapide que i++.

Dmitry Isaev
la source
6

Pour faire court: il n'y a absolument aucune différence à le faire en JavaScript.

Tout d'abord, vous pouvez le tester vous-même:

Non seulement vous pouvez tester et exécuter n'importe quel script dans n'importe quelle bibliothèque JavaScript, mais vous avez également accès à l'ensemble des scripts précédemment écrits, ainsi qu'à la possibilité de voir les différences entre le temps d'exécution dans différents navigateurs sur différentes plates-formes.

Pour autant que vous puissiez voir, il n'y a aucune différence entre les performances dans n'importe quel environnement.

Si vous souhaitez améliorer les performances de votre script, vous pouvez essayer de faire les choses suivantes:

  1. Avoir une var a = array.length;déclaration pour que vous ne calculiez pas sa valeur à chaque fois dans la boucle
  2. Faites dérouler la boucle http://en.wikipedia.org/wiki/Loop_unwinding

Mais vous devez comprendre que l'amélioration que vous pouvez obtenir sera si insignifiante que vous ne devriez même pas vous en soucier.

Ma propre opinion pourquoi une telle idée fausse (Dec vs Inc) est apparue

Il y a très longtemps, il y avait une instruction machine commune, DSZ (décrémentation et saut sur zéro). Les personnes qui ont programmé en langage assembleur ont utilisé cette instruction pour implémenter des boucles afin de sauvegarder un registre. Maintenant, ces faits anciens sont obsolètes, et je suis sûr que vous n'obtiendrez aucune amélioration des performances dans aucune langue en utilisant cette pseudo amélioration.

Je pense que la seule façon dont de telles connaissances peuvent se propager à notre époque est lorsque vous lisez le code personne d'un autre. Voir une telle construction et demander pourquoi a-t-elle été implémentée et voici la réponse: "elle améliore les performances car elle se compare à zéro". Vous êtes devenu déconcerté par une meilleure connaissance de votre collègue et pensez à l'utiliser pour être beaucoup plus intelligent :-)

Salvador Dali
la source
Intéressant, mais pour vos tests exécutés sous firefox 16.0.2 sur win7, la boucle de décrémentation était 29% plus lente ... EDIT: Ne tenez pas compte de cela. Les tests répétés se sont révélés peu concluants, il y a une quantité surprenante de bruit dans mes résultats de test pour les essais ultérieurs. Je ne sais pas trop pourquoi.
Oui, j'ai essayé de tenir compte de cela en fermant tout le reste et en exécutant simplement les tests. Toujours obtenu des résultats bancaux. Très étrange.
Je pense que vous avez manqué le vrai point pourquoi le passage à zéro est considéré comme meilleur en JavaScript. C'est principalement parce que de cette façon, une seule variable contrôle l'exécution de la boucle, par exemple, de cette façon, l'optimiseur / JITer a plus de marge d'amélioration. L'utilisation array.lengthn'entraîne pas nécessairement une baisse des performances, simplement parce que la machine virtuelle JS est suffisamment intelligente pour déterminer si le tableau n'est pas modifié par le corps de la boucle. Voir ma réponse ci-dessous.
Pavel P
1
nitpicking: ce fait ancien (optimisations du langage d'assemblage) n'est pas obsolète, juste obscur. comme en vous n'avez pas besoin de savoir à moins que vous ne le fassiez vraiment. :-)
Javier
6

J'ai fait une comparaison sur jsbench .

Comme l'a souligné alestani, une chose qui prend du temps dans les boucles ascendantes, est d'évaluer, pour chaque itération, la taille de votre tableau. Dans cette boucle:

for ( var i = 1; i <= array.length; i++ )

vous évaluez .lengthchaque fois que vous augmentezi . Dans celui-ci:

for ( var i = 1, l = array.length; i <= l; i++ )

vous évaluez .length qu'une seule fois, lorsque vous déclarez i. Dans celui-ci:

for ( var i = array.length; i--; )

la comparaison est implicite, elle se produit juste avant la décrémentation iet le code est très lisible. Cependant, ce qui peut faire une énorme différence, c'est ce que vous mettez dans la boucle.

Boucle avec appel à la fonction (définie ailleurs):

for (i = values.length; i-- ;) {
  add( values[i] );
}

Boucle avec code intégré:

var sum = 0;
for ( i = values.length; i-- ;) {
  sum += values[i];
}

Si vous pouvez aligner votre code, au lieu d'appeler une fonction, sans sacrifier la lisibilité, vous pouvez avoir une boucle d'un ordre de grandeur plus rapidement!


Remarque : le navigateur devient bon à intégrer des fonctions simples, cela dépend vraiment de la complexité de votre code. Donc, profil avant optimisation, car

  1. Le goulot d'étranglement peut être ailleurs (ajax, refusion, ...)
  2. Vous pouvez choisir un meilleur algorithme
  3. Vous pouvez choisir une meilleure structure de données

Mais rappelles-toi:

Le code est écrit pour que les gens lisent, et accessoirement pour les machines à exécuter.

Spyryto
la source
+1 pour cette réponse et pour l'indice de référence. J'ai ajouté des tests forEach et retravaillé la référence à un fichier autonome exécutable dans le navigateur ainsi que dans le nœud. jsfiddle.net/hta69may/2 . Pour le nœud "Boucle inversée, comparaison implicite, code en ligne" est le plus rapide. Mais les tests en FF 50 ont montré des résultats curieux: non seulement les temps étaient presque 10 fois moins (!), Mais les deux tests "forEach" étaient aussi rapides que la "boucle inverse". Peut-être que les gars de Node devraient utiliser le moteur JS de Mozilla au lieu de V8? :)
Fr0sT
4

La façon dont vous le faites maintenant n'est pas plus rapide (à part qu'il s'agit d'une boucle indéfinie, je suppose que vous vouliez le faire i--.

Si vous voulez accélérer les choses, faites:

for (i = 10; i--;) {
    //super fast loop
}

bien sûr, vous ne le remarqueriez pas sur une si petite boucle. La raison pour laquelle c'est plus rapide est parce que vous décrémentez i tout en vérifiant que c'est "vrai" (il est évalué à "faux" quand il atteint 0)

peirix
la source
1
Vous manquez un point-virgule? (i = 10; i--;)
searlea
Oui oui, j'ai corrigé l'erreur. Et si nous voulons être pointilleux, je soulignerai que vous avez oublié votre point-virgule après votre i--! Il h.
djdd87
Pourquoi serait-ce plus rapide? Une source plus courte ne signifie pas qu'elle sera plus rapide. L'avez-vous mesuré?
Greg Hewgill
4
Oui, je l'ai mesuré, et je ne dis pas qu'une source plus courte le rend plus rapide, mais moins d'opérations le rend plus rapide.
peirix
4

Parfois, apporter des modifications très mineures à la façon dont nous écrivons notre code peut faire une grande différence dans la rapidité d'exécution de notre code. Un domaine où un changement de code mineur peut faire une grande différence dans les temps d'exécution est celui où nous avons une boucle for qui traite un tableau. Lorsque le tableau contient des éléments sur la page Web (tels que des boutons radio), le changement a le plus grand effet, mais il vaut toujours la peine d'appliquer ce changement même lorsque le tableau est interne au code Javascript.

La manière conventionnelle de coder une boucle for pour traiter un tableau est comme ceci:

for (var i = 0; i < myArray.length; i++) {...

Le problème avec cela est que l'évaluation de la longueur du tableau à l'aide de myArray.length prend du temps et la façon dont nous avons codé la boucle signifie que cette évaluation doit être effectuée à chaque fois autour de la boucle. Si le tableau contient 1000 éléments, la longueur du tableau sera évaluée 1001 fois. Si nous regardions les boutons radio et que nous avions myForm.myButtons.length, il faudra encore plus de temps pour évaluer car le groupe de boutons approprié dans le formulaire spécifié doit d'abord être localisé avant que la longueur puisse être évaluée à chaque fois dans la boucle.

Évidemment, nous ne nous attendons pas à ce que la longueur du tableau change pendant que nous le traitons, donc tous ces recalculs de la longueur ne font qu'ajouter inutilement au temps de traitement. (Bien sûr, si vous avez du code dans la boucle qui ajoute ou supprime des entrées de tableau, la taille du tableau peut changer entre les itérations et nous ne pouvons donc pas changer le code qui le teste)

Ce que nous pouvons faire pour corriger cela pour une boucle où la taille est fixe est d'évaluer la longueur une fois au début de la boucle et de l'enregistrer dans une variable. Nous pouvons ensuite tester la variable pour décider quand terminer la boucle. C'est beaucoup plus rapide que d'évaluer la longueur du tableau à chaque fois, en particulier lorsque le tableau contient plus que quelques entrées ou fait partie de la page Web.

Le code pour ce faire est:

for (var i = 0, var j = myArray.length; i < j; i++) {...

Alors maintenant, nous n'évaluons la taille du tableau qu'une seule fois et testons notre compteur de boucles par rapport à la variable qui détient cette valeur à chaque fois dans la boucle. Cette variable supplémentaire est accessible beaucoup plus rapidement que l'évaluation de la taille du tableau et notre code s'exécutera donc beaucoup plus rapidement qu'auparavant. Nous avons juste une variable supplémentaire dans notre script.

Souvent, peu importe l'ordre dans lequel nous traitons le tableau tant que toutes les entrées du tableau sont traitées. Dans ce cas, nous pouvons rendre notre code légèrement plus rapide en supprimant la variable supplémentaire que nous venons d'ajouter et en traitant le tableau dans l'ordre inverse.

Le code final qui traite notre tableau de la manière la plus efficace possible est:

for (var i = myArray.length-1; i > -1; i--) {...

Ce code n'évalue toujours la taille du tableau qu'une seule fois au début, mais au lieu de comparer le compteur de boucle avec une variable, nous le comparons avec une constante. Puisqu'une constante est encore plus efficace d'accès qu'une variable et que nous avons une instruction d'affectation de moins qu'avant, notre troisième version du code est maintenant légèrement plus efficace que la deuxième version et beaucoup plus efficace que la première.

Sid
la source
4

++vs. --n'a pas d'importance car JavaScript est un langage interprété, pas un langage compilé. Chaque instruction se traduit par plus d'un langage machine et vous ne devez pas vous soucier des détails sanglants.

Les gens qui parlent d'utiliser --(ou ++) pour utiliser efficacement les instructions de montage ont tort. Ces instructions s'appliquent à l'arithmétique des entiers et il n'y a pas d'entiers en JavaScript, juste des nombres .

Vous devez écrire du code lisible.

Salman A
la source
3

Dans de nombreux cas, cela n'a essentiellement rien à voir avec le fait que les processeurs peuvent se comparer à zéro plus rapidement que les autres comparaisons.

En effet, seuls quelques moteurs Javascript (ceux de la liste JIT) génèrent réellement du code en langage machine.

La plupart des moteurs Javascript construisent une représentation interne du code source qu'ils interprètent ensuite (pour avoir une idée de ce à quoi cela ressemble, jetez un œil au bas de cette page sur SpiderMonkey de Firefox ). Généralement, si un morceau de code fait pratiquement la même chose mais conduit à une représentation interne plus simple, il s'exécutera plus rapidement.

Gardez à l'esprit qu'avec des tâches simples telles que l'ajout / la soustraction d'une variable ou la comparaison d'une variable à quelque chose, la surcharge de l'interpréteur passant d'une "instruction" interne à la suivante est assez élevée, donc les moins "instructions" qui sont utilisé en interne par le moteur JS, mieux c'est.

Artelius
la source
3

Eh bien, je ne sais pas pour JavaScript, cela devrait vraiment être juste une question de longueur de tableau de réévaluation et peut-être quelque chose à voir avec les tableaux associatifs (si vous décrémentez seulement, il est peu probable que de nouvelles entrées soient allouées - si le tableau est dense, c'est-à-dire que quelqu'un peut optimiser pour cela).

Dans un assemblage de bas niveau, il existe une instruction de bouclage, appelée DJNZ (décrémentation et saut si non nul). Ainsi, la décrémentation et le saut sont tous dans une instruction, ce qui la rend probablement un peu plus rapide que INC et JL / JB (incrémenter, sauter si moins que / sauter si ci-dessous). En outre, la comparaison avec zéro est plus simple que la comparaison avec un autre nombre. Mais tout cela est vraiment marginal et dépend également de l'architecture cible (pourrait faire la différence, par exemple, sur Arm dans un smartphone).

Je ne m'attendrais pas à ce que ces différences de bas niveau aient un si grand impact sur les langages interprétés, je n'ai tout simplement pas vu DJNZ parmi les réponses, alors j'ai pensé partager une pensée intéressante.

le porc
la source
Pour mémoire, DJNZest une instruction dans le 8051 (z80) ISA. x86 a dec/jnzau lieu de inc/cmp/jne, et apparemment arm a quelque chose de similaire. Je suis sûr que ce n'est pas la cause de la différence Javascript; c'est d'avoir plus à évaluer dans la condition de boucle.
Peter Cordes
3

Il a utilisé à dire que -I était plus rapide (en C ++) parce qu'il n'y a qu'un seul résultat, la valeur décrémentée. i-- doit stocker la valeur décrémentée sur i et conserver également la valeur d'origine comme résultat (j = i--;). Dans la plupart des compilateurs, cela utilisait deux registres plutôt qu'un seul, ce qui pouvait entraîner l'écriture d'une autre variable en mémoire plutôt que sa conservation en tant que variable de registre.

Je suis d'accord avec les autres qui ont dit que cela ne fait aucune différence ces jours-ci.

Fourmi
la source
3

En termes très simples

"i-- et i ++. En fait, ils prennent tous les deux le même temps".

mais dans ce cas, lorsque vous avez un fonctionnement incrémentiel .. le processeur évalue la .length chaque fois que la variable est incrémentée de 1 et en cas de décrémentation .. en particulier dans ce cas, il n'évaluera .length qu'une seule fois jusqu'à ce que nous obtenions 0.

Ravi_Parmar
la source
3

Tout d'abord, i++et i--prenez exactement le même temps sur n'importe quel langage de programmation, y compris JavaScript.

Le code suivant prend beaucoup de temps.

Vite:

for (var i = 0, len = Things.length - 1; i <= len; i++) { Things[i] };

Lent:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

Par conséquent, le code suivant prend également un temps différent.

Vite:

for (var i = Things.length - 1; i >= 0; i--) { Things[i] };

Lent:

for (var i = 0; i <= Things.length - 1; i++) { Things[i] };

PS Slow n'est lent que pour quelques langues (moteurs JavaScript) en raison de l'optimisation du compilateur. La meilleure façon est d'utiliser '<' à la place '<=' (ou '=') et '--i' à la place 'i--' .

Dmitry
la source
3

Pas beaucoup de temps est consommé par i-- ou i ++. Si vous allez profondément dans l'architecture du processeur, le ++est plus rapide que le --, car l' --opération fera le complément du 2, mais elle s'est produite à l'intérieur du matériel, ce qui le rendra rapide et aucune différence majeure entre le ++et --ces opérations sont également considérées comme le le moins de temps consommé dans le CPU.

La boucle for fonctionne comme ceci:

  • Initialisez la variable une fois au début.
  • Vérifiez la contrainte dans le second opérande de la boucle, <, >, <=, etc.
  • Appliquez ensuite la boucle.
  • Incrémentez la boucle et relancez à nouveau ces processus.

Alors,

for (var i = Things.length - 1; i >= 0; i--) {
    Things[i]
}; 

va calculer la longueur du tableau une seule fois au début et ce n'est pas beaucoup de temps, mais

for(var i = array.length; i--; ) 

calculera la longueur à chaque boucle, donc cela prendra beaucoup de temps.

Omar Freewan
la source
var i = Things.length - 1; i >= 0; i--calculera également la durée 1 fois.
Dmitry
1
Je ne sais pas ce que "l' --opération fera le complément à 2" signifie, mais je suppose que cela signifie qu'il annulera quelque chose. Non, cela n'annule rien sur aucune architecture. Soustraire 1 est aussi simple que d'ajouter 1, vous faites juste un circuit qui emprunte plutôt que porte.
Olathe
1

La meilleure approche pour répondre à ce genre de question est de l'essayer. Configurez une boucle qui compte un million d'itérations ou autre, et faites-le dans les deux sens. Minutez les deux boucles et comparez les résultats.

La réponse dépendra probablement du navigateur que vous utilisez. Certains auront des résultats différents des autres.

Greg Hewgill
la source
4
Cela ne répondrait toujours pas à sa question de savoir pourquoi c'est plus rapide. Il venait juste d'obtenir un point de repère, sans savoir pourquoi il en était ainsi ...
peirix
3
C'est vrai. Cependant, sans connaître l'implémentation exacte de chaque moteur Javascript dans chaque navigateur, il est presque impossible de répondre à la partie "pourquoi". Beaucoup de réponses ici contiennent des recommandations anecdotiques comme "utiliser le prédécrément au lieu du post-décrément" (une astuce d'optimisation C ++) et "comparer avec zéro", ce qui pourrait être vrai dans les langages compilés en code natif, mais Javascript est assez loin du métal nu du CPU.
Greg Hewgill
4
-1 Je suis complètement en désaccord avec l'idée que la meilleure approche est de l'essayer. Quelques exemples ne remplacent pas la connaissance collective, et c'est tout l'intérêt de forums comme celui-ci.
Christophe
1

J'adore, beaucoup de marques mais pas de réponse: D

Mettre simplement une comparaison contre zéro est toujours la comparaison la plus rapide

Donc (a == 0) est en fait plus rapide à renvoyer True que (a == 5)

C'est petit et insignifiant et avec 100 millions de lignes dans une collection, il est mesurable.

c'est-à-dire sur une boucle, vous pourriez dire où i <= array.length et incrémenter i

sur une boucle descendante, vous pourriez dire où i> = 0 et décrémenter i à la place.

La comparaison est plus rapide. Pas la «direction» de la boucle.

Robert
la source
Il n'y a pas de réponse à la question comme indiqué car les moteurs Javascript sont tous différents et la réponse dépend exactement du navigateur sur lequel vous le mesurez.
Greg Hewgill
Non, les comparaisons fondamentalement acceptées contre zéro sont les plus rapides. Bien que vos déclarations soient également correctes, la règle d'or de la comparaison avec zéro est absolue.
Robert
4
Cela n'est vrai que si le compilateur choisit de faire cette optimisation, ce qui n'est certainement pas garanti. Le compilateur peut générer exactement le même code pour (a == 0) et (a == 5), à l'exception de la valeur de la constante. Un CPU ne se comparera pas plus rapidement à 0 qu'à aucune autre valeur, si les deux côtés de la comparaison sont des variables (du point de vue du CPU). Généralement, seuls les compilateurs de code natif ont la possibilité d'optimiser à ce niveau.
Greg Hewgill
1

AIDEZ LES AUTRES À ÉVITER UN MAUX DE TÊTE --- VOTER ICI !!!

La réponse la plus populaire sur cette page ne fonctionne pas pour Firefox 14 et ne passe pas le jsLinter. Les boucles "while" nécessitent un opérateur de comparaison, pas une affectation. Il fonctionne sur Chrome, Safari et même IE. Mais meurt dans Firefox.

C'EST CASSÉ!

var i = arr.length; //or 10
while(i--)
{
  //...
}

Cela fonctionnera! (fonctionne sur firefox, passe le jsLinter)

var i = arr.length; //or 10
while(i>-1)
{
  //...
  i = i - 1;
}
mrbinky3000
la source
2
Je viens de l'essayer sur Firefox 14 et cela a bien fonctionné. J'ai vu des exemples de bibliothèques de production qui utilisent while (i--)et qui fonctionnent sur de nombreux navigateurs. Aurait-il pu y avoir quelque chose de bizarre dans votre test? Utilisez-vous une version bêta de Firefox 14?
Matt Browne
1

C'est juste une supposition, mais c'est peut-être parce qu'il est plus facile pour le processeur de comparer quelque chose avec 0 (i> = 0) plutôt qu'avec une autre valeur (i <Things.length).

Rorchackh
la source
3
+1 Les autres n'ont-ils pas voté contre. Bien que l'évaluation répétée de .length soit beaucoup plus un problème que l'augmentation / décroissance réelle, la vérification de la fin de la boucle peut être importante. Mon compilateur C donne des avertissements sur les boucles comme: "remarque # 1544-D: (ULP 13.1) Décompte des boucles détectées. Recommande le décompte des boucles car la détection des zéros est plus facile"
harper