web-dev-qa-db-fra.com

Itérateurs C ++ et optimisation de boucle

Je vois beaucoup de code c ++ qui ressemble à ceci:

for( const_iterator it = list.begin(),
     const_iterator ite = list.end();
     it != ite; ++it)

Par opposition à la version plus concise:

for( const_iterator it = list.begin();
     it != list.end(); ++it)

Y aura-t-il une différence de vitesse entre ces deux conventions? Naïvement, le premier sera légèrement plus rapide car list.end () n'est appelé qu'une seule fois. Mais comme l'itérateur est const, il semble que le compilateur extraira ce test de la boucle, générant un assemblage équivalent pour les deux.

46
Quantum7

Je mentionnerai simplement pour mémoire que la norme C++ exige que l'appel de begin() et end() on any type de conteneur (que ce soit vector, list, map etc.) ne doit prendre qu'un temps constant. En pratique, ces appels seront presque certainement alignés sur une comparaison de pointeur unique si vous compilez avec des optimisations activées.

Notez que cette garantie ne s'applique pas nécessairement aux "conteneurs" supplémentaires fournis par le fournisseur qui ne respectent pas réellement les exigences formelles d'être un conteneur énoncées dans le chapitre 23 de la norme (par exemple la liste à liaison simple slist ).

29
j_random_hacker

Cependant, les deux versions ne sont pas identiques. Dans la deuxième version, il compare l'itérateur à list.end() à chaque fois, et ce que list.end() évalue peut changer au cours de la boucle. Maintenant, bien sûr, vous ne pouvez pas modifier list via const_iterator it; mais rien n'empêche le code à l'intérieur de la boucle d'appeler simplement des méthodes sur list directement et de le muter, ce qui pourrait (selon le type de structure de données list) changer l'itérateur de fin. Il peut donc être incorrect dans certaines circonstances de stocker l'itérateur final au préalable, car il se peut que ce ne soit plus le bon itérateur final au moment où vous y accédez.

43
newacct

Le premier sera probablement presque toujours plus rapide, mais si vous pensez que cela fera une différence, toujours profil en premier pour voir lequel est le plus rapide et de combien .

Le compilateur pourra probablement incorporer l'appel à end() dans les deux cas, bien que si end() soit suffisamment compliqué, il peut choisir de ne pas l'inline. Cependant, l'optimisation clé est de savoir si le compilateur peut effectuer mouvement de code invariant en boucle . Je suppose que dans la plupart des cas, le compilateur ne peut pas être certain que la valeur de end() ne changera pas pendant l'itération de la boucle, auquel cas il n'a pas d'autre choix que d'appeler end() après chaque itération.

11
Adam Rosenfield

Je choisirais l'option la plus concise et la plus lisible. N'essayez pas de deviner le compilateur et les optimisations qu'il pourrait effectuer. N'oubliez pas que la grande majorité de votre code n'aura absolument aucun effet sur les performances globales, donc seulement si cela se trouve dans une section de code critique pour les performances, vous devriez passer le temps de le profiler et de choisir une représentation source suffisamment efficace.

En se référant spécifiquement à votre exemple, la première version crée un copie de l'itérateur end(), invoquant le code exécuté pour le constructeur de copie de l'objet itérateur. Les conteneurs STL contiennent généralement des fonctions inline end(), donc le compilateur a de nombreuses possibilités d'optimiser la deuxième version même si vous n'essayez pas de l'aider. Laquelle est la meilleure? Mesurez-les.

8
Greg Hewgill

Considérez cet exemple:

for (const_iterator it = list.begin(); it != list.end(); ++list)
{
    if (moonFull())
        it = insert_stuff(list);
    else
        it = erase_stuff(list);
}

dans ce cas, vous DEVEZ appeler list.end () dans la boucle, et le compilateur ne va pas optimiser cela.

Dans d'autres cas où le compilateur peut prouver que end () renvoie toujours la même valeur, une optimisation peut avoir lieu.

Si nous parlons de conteneurs STL, je pense que tout bon compilateur peut optimiser plusieurs appels end () lorsque plusieurs appels end () ne sont pas nécessaires pour la logique de programmation. Cependant, si vous avez un conteneur personnalisé et que l'implémentation de end () n'est pas dans la même unité de traduction, l'optimisation devra se produire au moment de la liaison. Je sais très peu de choses sur l'optimisation du temps de liaison, mais je parie que la plupart des éditeurs de liens ne feront pas une telle optimisation.

6
Shing Yip

Vous pouvez rendre la première version plus concise et tirer le meilleur parti des deux:

for( const_iterator it = list.begin(), ite = list.end();
     it != ite; ++it)

P.S. Les itérateurs ne sont pas const, ils sont des itérateurs vers une référence const. Il y a une grande différence.

6
Mark Ransom

Le compilateur peut être en mesure d'optimiser le second dans le premier, mais cela suppose que les deux sont équivalents, c'est-à-dire que end () est en fait constant. Un problème légèrement plus problématique est que le compilateur peut être incapable de déduire que l'itérateur final est constant en raison d'un éventuel alias. Cependant, en supposant que l'appel à end () est en ligne, la différence n'est qu'une charge de mémoire.

Notez que cela suppose que l'optimiseur est activé. Si l'optimiseur n'est pas activé, comme c'est souvent le cas dans les versions de débogage, la deuxième formulation impliquera N-1 appels de fonction supplémentaires. Dans les versions actuelles de Visual C++, les versions de débogage entraîneront également des hits supplémentaires en raison des vérifications de prologue/épilogue de fonction et des itérateurs de débogage plus lourds. Par conséquent, dans le code lourd STL, la valeur par défaut du premier cas peut empêcher le code d'être excessivement lent dans les versions de débogage.

L'insertion et le retrait dans la boucle sont une possibilité, comme d'autres l'ont souligné, mais avec ce style de boucle, je trouve cela peu probable. D'une part, les conteneurs basés sur des nœuds - lister, définir, mapper - n'invalident pas end () sur aucune des opérations. Deuxièmement, l'incrément d'itérateur doit fréquemment être déplacé en boucle pour éviter les problèmes d'invalidation:

 // en supposant que la liste - ne peut pas mettre en cache end () pour le vecteur 
 itérateur it (c.begin ()), end (c.end ()); 
 while (it ! = end) {
 if (should_remove (* it)) 
 it = c.erase (it); 
 else 
 ++ it; 
}

Ainsi, je considère qu'une boucle qui prétend appeler end () pour des raisons de mutation pendant la boucle et qui l'a toujours ++ dans l'en-tête de boucle est suspecte.

4
Avery Lee

Aah, les gens semblent faire des suppositions. Ouvrez votre code dans le débogueur et vous verrez que les appels à begin (), end () etc. tout sont optimisés. Il n'est pas nécessaire d'utiliser la version 1. Testé avec le compilateur Visual C++ fullopt.

4
user15071
  1. Essayez-le dans des conditions de stress et voyez si vous êtes ** très souvent dans ce code ***.
    Sinon, cela n'a pas d'importance.

  2. Si vous l'êtes, regardez le démontage ou procédez en une seule étape.
    C'est ainsi que vous pouvez déterminer ce qui est plus rapide.

Vous devez faire attention à ces itérateurs.
Ils peuvent être optimisés en code machine Nice, mais assez souvent ils ne le font pas, et deviennent des porcs du temps.

** (Où "in" signifie réellement dedans, ou être appelé de lui.)

*** (Où "souvent" signifie un pourcentage significatif du temps.)

AJOUT: Ne voyez pas seulement combien de fois par seconde le code est exécuté. Cela pourrait être 1 000 fois par seconde et utiliser encore moins de 1% du temps.

Ne prenez pas le temps non plus. Cela pourrait prendre une milliseconde et utiliser encore moins de 1% du temps.

Vous pouvez multiplier les deux pour avoir une meilleure idée, mais cela ne fonctionne que s'ils ne sont pas trop biaisés.

Échantillonnage de la pile d'appels vous dira si elle utilise un pourcentage de temps suffisamment élevé pour être important.

2
Mike Dunlavey

J'ai toujours préféré le premier. Bien qu'avec les fonctions en ligne, les optimisations du compilateur et la taille du conteneur relativement plus petite (dans mon cas, c'est normalement max 20-25 articles), cela ne fait vraiment aucune grande différence en ce qui concerne la performance.

const_iterator it = list.begin();
const_iterator endIt = list.end();

for(; it != endIt ; ++it)
{//do something
}

Mais récemment, j'utilise plus de std :: for_each partout où c'est possible. Son bouclage optimisé qui aide à rendre le code plus lisible que les deux autres.

std::for_each(list.begin(), list.end(), Functor());

Je n'utiliserai la boucle que lorsque std::for_each Ne peut pas être utilisé. (par exemple: std::for_each ne vous permet pas de rompre la boucle sauf si une exception est levée).

1
aJ.

En théorie, le compilateur pourrait optimiser la deuxième version dans la première (en supposant que le conteneur ne change pas pendant la boucle, évidemment).

Dans la pratique, j'ai trouvé plusieurs cas similaires lors du profilage de code à temps critique où mon compilateur n'a pas totalement réussi à sortir les calculs invariants des conditions de boucle. Donc, bien que la version légèrement plus concise soit correcte dans la plupart des cas, je ne compte pas sur le compilateur pour faire des choses sensées avec lui dans un cas où je suis vraiment préoccupé par les performances.

0
Peter