std :: vector (ab) utilise le stockage automatique

46

Considérez l'extrait de code suivant:

#include <array>
int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  huge_type t;
}

Évidemment, cela planterait sur la plupart des plates-formes, car la taille de pile par défaut est généralement inférieure à 20 Mo.

Considérez maintenant le code suivant:

#include <array>
#include <vector>

int main() {
  using huge_type = std::array<char, 20*1024*1024>;
  std::vector<huge_type> v(1);
}

Étonnamment, il se bloque également! Le traceback (avec l'une des versions récentes de libstdc ++) mène au include/bits/stl_uninitialized.hfichier, où nous pouvons voir les lignes suivantes:

typedef typename iterator_traits<_ForwardIterator>::value_type _ValueType;
std::fill(__first, __last, _ValueType());

Le vectorconstructeur de redimensionnement doit initialiser par défaut les éléments, et c'est ainsi qu'il est implémenté. De toute évidence, _ValueType()temporaire bloque la pile.

La question est de savoir s'il s'agit d'une implémentation conforme. Si oui, cela signifie en fait que l'utilisation d'un vecteur de types énormes est assez limitée, n'est-ce pas?

Igor R.
la source
Il ne faut pas stocker d'énormes objets dans un type de tableau. Cela nécessite potentiellement une très grande région de mémoire contigüe qui peut ne pas être présente. Au lieu de cela, ayez un vecteur de pointeurs (std :: unique_ptr typiquement) afin de ne pas placer une telle demande sur votre mémoire.
NathanOliver
2
Juste de la mémoire. Il existe des implémentations C ++ en cours d'exécution qui n'utilisent pas de mémoire virtuelle.
NathanOliver
3
Quel compilateur, au fait? Je ne peux pas reproduire avec VS 2019 (16.4.2)
ChrisMM
3
En regardant le code libstdc ++, cette implémentation n'est utilisée que si le type d'élément est trivial et copiable et si la valeur par défaut std::allocatorest utilisée.
noyer
1
@Damon Comme je l'ai mentionné ci-dessus, il ne semble être utilisé que pour les types triviaux avec l'allocateur par défaut, il ne devrait donc pas y avoir de différence observable.
noyer

Réponses:

19

Il n'y a pas de limite sur la quantité de stockage automatique utilisée par l'API std.

Ils pourraient tous nécessiter 12 téraoctets d'espace de pile.

Cependant, cette API nécessite uniquement Cpp17DefaultInsertableet votre implémentation crée une instance supplémentaire par rapport à ce qui est requis par le constructeur. Sauf si elle est bloquée derrière la détection de l'objet est trivialement modifiable et copiable, cette implémentation semble illégale.

Yakk - Adam Nevraumont
la source
8
En regardant le code libstdc ++, cette implémentation n'est utilisée que si le type d'élément est trivial et copiable et si la valeur par défaut std::allocatorest utilisée. Je ne sais pas pourquoi ce cas spécial est fabriqué en premier lieu.
noyer
3
@walnut Ce qui signifie que le compilateur est libre de créer comme si ce n'était pas réellement cet objet temporaire; Je suppose qu'il y a de bonnes chances pour une version optimisée qu'elle ne soit pas créée?
Yakk - Adam Nevraumont
4
Oui, je suppose que oui, mais pour les gros éléments, GCC ne semble pas le faire. Clang avec libstdc ++ optimise le temporaire, mais il semble que si la taille du vecteur passée au constructeur est une constante au moment de la compilation, voir godbolt.org/z/-2ZDMm .
noyer
1
@walnut le cas spécial est là pour que nous expédions vers std::filldes types triviaux, qui utilise ensuite memcpypour dynamiser les octets dans des endroits, ce qui est potentiellement beaucoup plus rapide que de construire de nombreux objets individuels dans une boucle. Je crois que l'implémentation de libstdc ++ est conforme, mais provoquer un débordement de pile pour les objets énormes est un bogue de qualité d'implémentation (QoI). Je l'ai signalé sous gcc.gnu.org/PR94540 et le corrigerai.
Jonathan Wakely
@JonathanWakely Oui, cela a du sens. Je ne me souviens pas pourquoi je n'y ai pas pensé quand j'ai écrit mon commentaire. Je suppose que j'aurais pensé que le premier élément construit par défaut serait construit directement sur place et que l'on pourrait copier à partir de cela, de sorte qu'aucun objet supplémentaire du type d'élément ne sera jamais construit. Mais bien sûr, je n'ai pas vraiment réfléchi à cela en détail et je ne connais pas les avantages et les inconvénients de la mise en œuvre de la bibliothèque standard. (J'ai réalisé trop tard que c'est aussi votre suggestion dans le rapport de bogue.)
noyer
9
huge_type t;

Évidemment, cela planterait sur la plupart des plates-formes ...

Je conteste l'hypothèse de "la plupart". Étant donné que la mémoire de l'objet énorme n'est jamais utilisée, le compilateur peut l'ignorer complètement et ne jamais allouer la mémoire, auquel cas il n'y aurait pas de plantage.

La question est de savoir s'il s'agit d'une implémentation conforme.

La norme C ++ ne limite pas l'utilisation de la pile, ni ne reconnaît même l'existence d'une pile. Donc, oui, il est conforme à la norme. Mais on pourrait considérer qu'il s'agit d'un problème de qualité de mise en œuvre.

cela signifie en fait que l'utilisation d'un vecteur de types énormes est assez limitée, n'est-ce pas?

Cela semble être le cas avec libstdc ++. Le crash n'a pas été reproduit avec libc ++ (en utilisant clang), il semble donc que ce ne soit pas une limitation dans le langage, mais plutôt uniquement dans cette implémentation particulière.

eerorika
la source
6
"ne plantera pas nécessairement malgré le débordement de la pile car la mémoire allouée n'est jamais accessible par le programme" - si la pile est utilisée de quelque manière que ce soit par la suite (par exemple pour appeler une fonction), cela se bloquera même sur les plates-formes à sur-validation .
Ruslan
Toute plate-forme sur laquelle cela ne se bloque pas (en supposant que l'objet n'est pas alloué avec succès) est vulnérable à Stack Clash.
user253751
@ user253751 Il serait optimiste de supposer que la plupart des plateformes / programmes ne sont pas vulnérables.
eerorika
Je pense que le sur-engagement ne s'applique qu'au tas, pas à la pile. La pile a une limite supérieure fixe sur sa taille.
Jonathan Wakely
@JonathanWakely Vous avez raison. Il semble que la raison pour laquelle il ne se bloque pas est que le compilateur n'alloue jamais l'objet inutilisé.
eerorika
5

Je ne suis ni juriste ni expert en standard C ++, mais cppreference.com dit:

explicit vector( size_type count, const Allocator& alloc = Allocator() );

Construit le conteneur avec le nombre d'instances de T. insérées par défaut. Aucune copie n'est effectuée.

Peut-être que je comprends mal "inséré par défaut", mais je m'attendrais à:

std::vector<huge_type> v(1);

être équivalent à

std::vector<huge_type> v;
v.emplace_back();

Cette dernière version ne doit pas créer de copie de pile mais construire un énorme_type directement dans la mémoire dynamique du vecteur.

Je ne peux pas dire avec autorité que ce que vous voyez n'est pas conforme, mais ce n'est certainement pas ce que j'attendrais d'une implémentation de qualité.

Adrian McCarthy
la source
4
Comme je l'ai mentionné dans un commentaire sur la question, libstdc ++ n'utilise cette implémentation que pour les types triviaux avec affectation de copie et std::allocator, il ne devrait donc pas y avoir de différence observable entre l'insertion directe dans la mémoire des vecteurs et la création d'une copie intermédiaire.
noyer
@walnut: D'accord, mais l'énorme allocation de pile et l'impact sur les performances de l'initialisation et de la copie sont toujours des choses que je n'attendrais pas d'une implémentation de haute qualité.
Adrian McCarthy
2
Oui je suis d'accord. Je pense que c'était un oubli dans la mise en œuvre. Mon point était seulement que cela n'a pas d'importance en termes de conformité standard.
noyer
IIRC vous avez également besoin de la possibilité de copier ou de déplacer pour emplace_backmais pas seulement pour créer un vecteur. Ce qui signifie que vous pouvez avoir vector<mutex> v(1)mais pas vector<mutex> v; v.emplace_back();Pour quelque chose comme huge_typevous pourriez avoir encore une opération d'allocation et de déplacement avec la deuxième version. Aucun des deux ne doit créer d'objets temporaires.
dyp
1
@IgorR. vector::vector(size_type, Allocator const&)requiert (Cpp17) DefaultInsertable
dyp