web-dev-qa-db-fra.com

Pourquoi std :: vector reserve ne "double" pas sa capacité, alors que le redimensionnement le fait?

Je viens de découvrir que std::vector<T>::resize "Double" sa capacité même lors du redimensionnement d'un élément au-dessus de la taille actuelle:

std::vector<int> v(50);
v.resize(51);
std::cout << v.capacity() << std::endl;

Ce programme génère 100 avec GCC et Clang et 75 avec Visual C++. Cependant, lorsque je passe de resize à reserve:

std::vector<int> v(50);
v.reserve(51);
std::cout << v.capacity() << std::endl;

La sortie est de 51 avec les trois compilateurs.

Je me demande pourquoi les implémentations utilisent une stratégie d'expansion différente pour resize et reserve. Cela semble incohérent, et je m'attendrais au même comportement ici.


J'ajoute simplement un lien vers une motivation pour ma question, où l'impact sur les performances est signalé: Pourquoi les vecteurs C++ STL 1000x sont-ils plus lents quand on fait beaucoup de réserves?


Ajout d'un devis à partir de la norme C++ 11 pour clarifier les exigences pour reserve; §23.3.6.3 (2):

Après reserve(), capacity() est supérieur ou égal à l'argument de reserve si réallocation arrive...


Quelques réflexions supplémentaires: De la norme C++ 11:

Complexité: La complexité est linéaire dans le nombre d'éléments insérés plus la distance jusqu'à la fin du vecteur.

Ce qui, effectivement, implique une complexité constante (amortie) pour l'insertion d'un seul élément à la fin. Cependant, cela ne s'applique qu'aux modificateurs de vecteur , tels que Push_back Ou insert (§23.3.6.5).

resize n'est pas répertorié parmi les modificateurs. Il est répertorié dans le §23.3.6.3 vector section de capacité. Et, il n'y a aucune exigence de complexité pour resize.

Cependant, dans la section vector overview (§23.3.6.1), il est écrit:

il (vector) supporte (amorti) les opérations d'insertion et d'effacement à temps constant à la fin

La question est de savoir si resize(size()+1) est considéré comme "insertion à la fin".

25
Daniel Langr

Pour autant que je sache, ni resize ni reserve n'est requis pour avoir le comportement démontré. Les deux sont toutefois autorisés à adopter un tel comportement, bien que les deux puissent soit allouer le montant exact, soit multiplier l'allocation précédente en ce qui concerne la norme.

Chaque stratégie d'allocation a ses avantages. L'avantage d'allouer le montant exact est qu'il n'a pas de surcharge de mémoire lorsque l'allocation maximale est connue au préalable. L'avantage de la multiplication est qu'elle maintient la propriété amortie constante lorsqu'elle est mélangée avec des opérations d'insertion finale.

L'approche choisie par les implémentations testées présente l'avantage de permettre les deux stratégies lors du redimensionnement. Pour utiliser une stratégie, on peut réserver puis redimensionner. Pour utiliser l'autre, redimensionnez simplement. Bien sûr, il faut être conscient du comportement non spécifié pour en profiter. Cet avantage peut ou non être le raisonnement derrière le choix de ces implémentations.

On pourrait considérer comme un échec de l'API vectorielle, comme spécifié dans la norme, qu'il n'est pas possible d'exprimer le comportement de réallocation prévu (d'une manière qui est garantie par la norme).

17
eerorika

Lorsque vous resize plus qu'il n'y a de capacité, vous "démontrez" déjà que vous ne voulez pas réserver uniquement la bonne capacité. En revanche, si vous utilisez reserve, vous demandez explicitement la bonne capacité. Si reserve utiliserait la même stratégie que resize, il n'y aurait aucun moyen de réserver juste le bon montant.

En ce sens resize sans reserve est pour les paresseux ou au cas où vous ne connaissez pas le montant exact à réserver. Vous appelez reserve si vous savez de quelle capacité vous avez besoin. Voilà deux scénarios différents.

PS: Comme l'a souligné StoryTeller, reserve n'est pas non plus tenu de réserver le montant exact demandé conformément à la norme. Néanmoins, je pense que mon argument principal tient toujours: resize (sans reserve) et reserve sont destinés à différents scénarios, où vous donnez soit une indication de combien vous voulez réserver ou ne se soucient pas de la capacité réelle et veulent juste que le conteneur soit dimensionné selon ce que vous demandez.

Pourquoi vous attendriez-vous à ce qu'ils se comportent de la même façon? reserve est utilisé pour pré-allouer l'espace que vous utiliserez plus tard, dans l'attente que l'utilisateur ait une poignée décente sur la taille finale attendue du conteneur. resize est simplement une allocation normale et suit donc l'approche normale, efficace en termes de vitesse, d'augmenter géométriquement l'espace alloué au conteneur.

La taille des conteneurs augmente par étapes multiplicatives pour réduire le nombre d'allocations nécessaires et ainsi maintenir la vitesse et réduire la fragmentation de la mémoire. Le doublement est le plus courant, mais certaines implémentations utilisent des étapes de 1,5 (par exemple MSVC) qui échangent des allocations accrues pour un espace gaspillé inférieur dans chaque conteneur.

Mais, si l'utilisateur a déjà indiqué à la bibliothèque la taille qu'il pense que le conteneur obtiendra - en provoquant reserve - il n'est pas nécessaire d'allouer de l'espace en excès, il peut à la place faire confiance à l'utilisateur pour l'avoir appelé avec le bon numéro. . C'est reserve qui a le comportement inhabituel, pas resize.

6
Jack Aidley

resize doit suivre une stratégie de réallocation exponentielle pour remplir sa garantie de complexité (linéaire dans le nombre d'éléments inséré). Cela peut être vu en considérant que resize(size() + 1) doit avoir une complexité constante amortie, donc doit suivre une croissance exponentielle pour la même raison que Push_back (complexité constante amortie) doit croître de façon exponentielle.

Une implémentation de reserve est autorisée à suivre la stratégie d'allocation qu'elle souhaite, car sa seule exigence de complexité est qu'elle soit linéaire dans le nombre d'éléments présent. Cependant, si une mise en œuvre était par exemple pour arrondir à la puissance suivante de deux, cela serait peu efficace (et surprenant) dans le cas où l'utilisateur connaît exactement la quantité de mémoire requise, et pourrait compliquer le portage si l'utilisateur dépend de ce comportement. La latitude dans la norme est mieux exercée dans les cas où il n'y a pas d'inefficacité spatiale, par ex. en arrondissant les allocations à la taille de Word, si l'allocateur fonctionne à cette granularité.

6
ecatmur

reserve modifie la capacité, tandis que resize modifie la size.

capacity est le nombre d'éléments pour lesquels le conteneur a actuellement alloué de l'espace.

size est le nombre d'éléments dans le conteneur.

Lorsque vous allouez un vecteur vide, vous obtenez un capacity (espace AKA) par défaut. La taille est toujours 0, et lorsque vous ajoutez des éléments dans le vecteur, son incrément de taille. Lorsque la taille est égale à la capacité et que vous ajoutez plus d'articles, la capacité doit augmenter (généralement le double d'elle-même).

Le problème avec le vecteur est qu'il garantit une mémoire séquentielle, ce qui signifie que chaque nouvelle croissance d'allocation aura également besoin d'une copie de l'allocation précédente vers la nouvelle, au cas où il n'y aurait pas d'espace pour la nouvelle taille d'allocation dans l'ancienne zone de mémoire allouée.

Ici, le reserve peut vous aider, si vous connaissez les éléments max du vecteur. Lorsque vous utilisez reserve, il n'y aura qu'une seule allocation et aucune copie de mémoire, sauf si vous passez les éléments réservés.

Lorsque vous dites le nombre exact réservé, vous obtenez la mémoire exacte que vous avez demandée. Lorsque vous ajoutez simplement des éléments (même avec un redimensionnement, vous ne dites pas que vous n'ajouterez pas d'autres éléments.

1
SHR