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 dereserve
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".
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).
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
.
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é.
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.