web-dev-qa-db-fra.com

Quelle est la manière la plus efficace d'itérer un vecteur std :: et pourquoi?

En termes de complexité espace-temps, lequel des éléments suivants est le meilleur moyen d'itérer sur un vecteur std :: et pourquoi?

Voie 1:

for(std::vector<T>::iterator it = v.begin(); it != v.end(); ++it) {
    /* std::cout << *it; ... */
}

Voie 2:

for(std::vector<int>::size_type i = 0; i != v.size(); i++) {
    /* std::cout << v[i]; ... */
}

Voie 3:

for(size_t i = 0; i != v.size(); i++) {
    /* std::cout << v[i]; ... */
}

Voie 4:

for(auto const& value: a) {
     /* std::cout << value; ... */
41
Suhasis

Tout d'abord, Way 2 et Way sont identiques dans pratiquement toutes les implémentations de bibliothèque standard.

En dehors de cela, les options que vous avez publiées sont presque équivalentes. La seule différence notable est que dans Way 1 et Way 2/, vous comptez sur le compilateur pour optimiser l'appel à v.end() et v.size() en dehors. Si cette hypothèse est correcte, il n'y a pas de différence de performances entre les boucles.

Si ce n'est pas le cas, Way 4 est le plus efficace. Rappelez-vous comment une plage basée sur une boucle s'étend jusqu'à

{
   auto && __range = range_expression ;
   auto __begin = begin_expr ;
   auto __end = end_expr ;
   for ( ; __begin != __end; ++__begin) {
      range_declaration = *__begin;
      loop_statement
   }
}

La partie importante ici est que cela garantit que le end_expr Ne sera évalué qu'une seule fois. Notez également que pour que la plage basée sur la boucle soit l'itération la plus efficace, vous ne devez pas modifier la façon dont le déréférencement de l'itérateur est géré, par ex.

for (auto value: a) { /* ... */ }

cela copie chaque élément du vecteur dans la variable de boucle value, qui est probablement plus lente que for (const auto& value : a), selon la taille des éléments dans le vecteur.

Notez qu'avec les fonctionnalités d'algorithme parallèle en C++ 17, vous pouvez également essayer

#include <algorithm>
#include <execution>

std::for_each(std::par_unseq, a.cbegin(), a.cend(),
   [](const auto& e) { /* do stuff... */ });

mais si cela est plus rapide qu'une boucle ordinaire dépend de nombreux détails circonstanciels.

37
lubgr

Préférez les itérateurs aux index/clés.

Alors que pour vector ou array, il ne devrait y avoir aucune différence entre les deux formes1, c'est une bonne habitude de prendre d'autres conteneurs.

1Tant que vous utilisez [] Au lieu de .at() pour l'accès par index, bien sûr.


Mémorisez la fin.

Le recalcul de la limite à chaque itération est inefficace pour deux raisons:

  • En général: une variable locale n'est pas aliasée, ce qui est plus convivial pour l'optimiseur.
  • Sur les conteneurs autres que vectoriels: le calcul de la fin/taille pourrait être un peu plus cher.

Vous pouvez le faire en une seule ligne:

for (auto it = vec.begin(), end = vec.end(); it != end; ++it) { ... }

(Il s'agit d'une exception à l'interdiction générale de déclarer une seule variable à la fois.)


Utilisez le formulaire de boucle for-each.

Le formulaire de boucle for-each:

  • Utilisez des itérateurs.
  • Mémorisez la fin.

Donc:

for (/*...*/ value : vec) { ... }

Prenez les types intégrés par valeurs, les autres types par référence.

Il existe un compromis non évident entre la prise d'un élément par valeur et la prise d'un élément par référence:

  • La prise d'un élément par référence évite une copie, ce qui peut être une opération coûteuse.
  • Prendre un élément en valeur est plus convivial pour l'optimiseur1.

Aux extrêmes, le choix doit être évident:

  • Les types intégrés (int, std::int64_t, void*, ...) doivent être pris par valeur.
  • Les types d'allocation potentielle (std::string, ...) doivent être pris par référence.

Au milieu, ou face à du code générique, je recommanderais de commencer par des références: il vaut mieux éviter une falaise de performances que d'essayer de faire sortir le dernier cycle.

Ainsi, la forme générale est:

for (auto& element : vec) { ... }

Et si vous avez affaire à un intégré:

for (int element : vec) { ... }

1Il s'agit en fait d'un principe général d'optimisation: les variables locales sont plus conviviales que les pointeurs/références car l'optimiseur connaît tous les alias potentiels (ou leur absence) de la variable locale.

12
Matthieu M.

Ajout à lubgr 's answer :

À moins que vous ne découvriez via le profilage que le code en question est un goulot d'étranglement, l'efficacité (que vous vouliez probablement dire au lieu de `` l'efficacité '') ne devrait pas être votre première préoccupation, du moins pas à ce niveau de code. La lisibilité et la maintenabilité du code sont bien plus importantes! Vous devez donc sélectionner la variante de boucle qui se lit le mieux, qui est généralement la voie 4.

Les indices peuvent être utiles si vous avez des étapes supérieures à 1 (pour autant que vous en ayez besoin ...):

for(size_t i = 0; i < v.size(); i += 2) { ... }

Tandis que += 2 en soi est également légal sur les itérateurs, vous risquez un comportement indéfini à la fin de la boucle si le vecteur a une taille étrange parce que vous incrémentez au-delà de celui au-delà de la position de fin! (Généralement parlé: si vous incrémentez de n , vous obtenez UB si la taille n'est pas un multiple exact de n .) Donc, vous avez besoin de code supplémentaire pour attraper cela, alors que vous ne le faites pas avec la variante d'index ...

10
Aconcagua

La réponse paresseuse: les complexités sont équivalentes.

  • La complexité temporelle de toutes les solutions est Θ (n).
  • La complexité spatiale de toutes les solutions est Θ (1).

Les facteurs constants impliqués dans les différentes solutions sont les détails de mise en œuvre. Si vous avez besoin de chiffres, vous feriez probablement mieux de comparer les différentes solutions sur votre système cible particulier.

Il peut être utile de stocker v.size() rsp. v.end(), bien que celles-ci soient généralement intégrées, de telles optimisations peuvent ne pas être nécessaires, ou exécutées automatiquement .

Notez que l'indexation (sans mémoriser v.size()) est le seul moyen de traiter correctement un corps de boucle qui peut ajouter des éléments supplémentaires (en utilisant Push_back()). Cependant, la plupart des cas d'utilisation n'ont pas besoin de cette flexibilité supplémentaire.

3
Arne Vogel

Préférez la méthode 4, std :: for_each (si vous le devez vraiment), ou la méthode 5/6:

void method5(std::vector<float>& v) {
    for(std::vector<float>::iterator it = v.begin(), e = v.end(); it != e; ++it) {
        *it *= *it; 
    }
}
void method6(std::vector<float>& v) {
    auto ptr = v.data();
    for(std::size_t i = 0, n = v.size(); i != n; i++) {
        ptr[i] *= ptr[i]; 
    }
}

Les 3 premières méthodes peuvent souffrir de problèmes d'alias de pointeur (comme mentionné dans les réponses précédentes) , mais elles sont toutes également mauvaises. Étant donné qu'il est possible qu'un autre thread puisse accéder au vecteur, la plupart des compilateurs le joueront en toute sécurité et réévalueront [] end () et size () à chaque itération. Cela empêchera toutes les optimisations SIMD.

Vous pouvez voir la preuve ici:

https://godbolt.org/z/Bchhm

Vous remarquerez que seuls 4/5/6 utilisent les instructions vmulps SIMD, alors que 1/2/3 n'utilisent que les instructions vmulss non SIMD.

Remarque: J'utilise VC++ dans le lien Godbolt car il illustre bien le problème. Le même problème se produit avec gcc/clang, mais il n'est pas facile de le démontrer avec godbolt - vous devez généralement démonter votre DSO pour voir cela se produire.

1
robthebloke

Cela dépend dans une large mesure de ce que vous entendez par "efficace".

D'autres réponses ont mentionné l'efficacité , mais je vais me concentrer sur l'objectif (IMO) le plus important du code C++: pour transmettre votre intention à d'autres programmeurs¹.

De ce point de vue, la méthode 4 est clairement la plus efficace. Non seulement parce qu'il y a moins de caractères à lire, mais surtout parce qu'il y a moins de charge cognitive : nous n'avons pas besoin de vérifier si les limites ou la taille des pas sont inhabituel, que la variable d'itération de boucle (i ou it) soit utilisée ou modifiée ailleurs, qu'il y ait une faute de frappe ou une erreur de copier/coller telle que for (auto i = 0u; i < v1.size(); ++i) { std::cout << v2[i]; }, ou des dizaines d'autres possibilités.

Quiz rapide: Compte tenu std::vector<int> v1, v2, v3;, combien des boucles suivantes sont correctes?

for (auto it = v1.cbegin();  it != v1.end();  ++it)
{
    std::cout << v1[i];
}

for (auto i = 0u;  i < v2.size();  ++i)
{
    std::cout << v1[i];
}

for (auto const i: v3)
{
    std::cout << i;
}

Exprimer le contrôle de boucle aussi clairement que possible permet au développeur de mieux comprendre la logique de haut niveau, plutôt que d'être encombré de détails d'implémentation - après tout, c'est pourquoi nous utilisons C++ en premier lieu!


¹ Pour être clair, lorsque j'écris du code, je considère que "l'autre programmeur" le plus important est Future Me, essayant de comprendre, " Qui a écrit ces ordures? "...

1
Toby Speight

Pour être complet, je voulais mentionner que votre boucle pourrait vouloir changer la taille du vecteur.

std::vector<int> v = get_some_data();
for (std::size_t i=0; i<v.size(); ++i)
{
    int x = some_function(v[i]);
    if(x) v.Push_back(x);
}

Dans un tel exemple, vous devez utiliser des indices et vous devez réévaluer v.size() à chaque itération.

Si vous faites de même avec une boucle for basée sur une plage ou avec des itérateurs, vous pourriez vous retrouver avec comportement non défini car l'ajout de nouveaux éléments à un vecteur peut invalider vos itérateurs.

Soit dit en passant, je préfère utiliser les boucles while- pour de tels cas plutôt que les boucles for- mais c'est une autre histoire.

1
Handy999

Toutes les façons que vous avez énumérées ont une complexité temporelle et une complexité spatiale identiques (pas de surprise là-bas).

L'utilisation de la syntaxe for(auto& value : v) est légèrement plus efficace, car avec les autres méthodes, le compilateur peut recharger v.size() et v.end() à partir de la mémoire chaque fois que vous effectuez le test , alors qu'avec for(auto& value : v) cela ne se produit jamais (il ne charge les itérateurs begin() et end() qu'une seule fois).

Nous pouvons observer une comparaison de l'assemblage produit par chaque méthode ici: https://godbolt.org/z/LnJF6p

Sur une note quelque peu drôle, le compilateur implémente method3 comme une instruction jmp à method2.

0
J. Antonio Perez

La complexité est la même pour tous sauf le dernier qui est en théorie plus rapide car la fin du conteneur n'est évaluée qu'une seule fois.

Le dernier est également le plus agréable à lire et à écrire, mais présente l'inconvénient de ne pas vous donner l'index (ce qui est souvent important).

Vous ignorez cependant ce que je pense être une bonne alternative (c'est ma préférée quand j'ai besoin de l'index et que je ne peux pas utiliser for (auto& x : v) {...}):

for (int i=0,n=v.size(); i<n; i++) {
    ... use v[i] ...
}

notez que j'ai utilisé int et non size_t et que la fin n'est calculée qu'une seule fois et est également disponible dans le corps en tant que variable locale.

Souvent, lorsque l'index et la taille sont nécessaires, des calculs mathématiques sont également effectués sur eux et size_t se comporte "étrangement" lorsqu'il est utilisé pour les mathématiques (par exemple a+1 < b et a < b-1 sont des choses différentes).

0
6502