web-dev-qa-db-fra.com

À quoi ressemble std :: vector en mémoire?

J'ai lu que std::vector Devrait être contigu. Je crois comprendre que ses éléments doivent être stockés ensemble et non répartis dans la mémoire. J'ai simplement accepté le fait et utilisé cette connaissance lorsque, par exemple, sa méthode data() a été utilisée pour obtenir la mémoire contiguë sous-jacente.

Cependant, je suis tombé sur une situation où la mémoire du vecteur se comporte de manière étrange:

std::vector<int> numbers;
std::vector<int*> ptr_numbers;
for (int i = 0; i < 8; i++) {
    numbers.Push_back(i);
    ptr_numbers.Push_back(&numbers.back());
}

Je m'attendais à ce que cela me donne un vecteur de quelques chiffres et un vecteur de pointeurs sur ces nombres. Cependant, lorsque vous répertoriez le contenu des pointeurs ptr_numbers, Il existe des nombres différents et apparemment aléatoires, comme si j'accédais à de mauvaises parties de la mémoire.

J'ai essayé de vérifier le contenu à chaque étape:

for (int i = 0; i < 8; i++) {
    numbers.Push_back(i);
    ptr_numbers.Push_back(&numbers.back());
    for (auto ptr_number : ptr_numbers)
       std::cout << *ptr_number << std::endl;
    std::cout << std::endl;
}

Le résultat ressemble à peu près à ceci:

1

some random number
2

some random number
some random number
3

Il semble donc que lorsque je Push_back() passe au vecteur numbers, ses éléments les plus anciens changent d’emplacement.

Alors qu'est-ce que cela signifie exactement, que std::vector Est un conteneur contigu et que ses éléments bougent? Est-ce que cela les stocke peut-être ensemble, mais les déplace-t-il quand plus d'espace est nécessaire?

Edit: Est-ce que std::vector Est contiguë seulement depuis C++ 17? (Juste pour que les commentaires sur ma revendication précédente soient pertinents pour les futurs lecteurs.)

38
McSim

Cela ressemble à peu près à ceci (excusez mon chef d’œuvre MS Paint):

vector memory layout

L'instance std::vector Que vous avez sur la pile est un petit objet contenant un pointeur sur un tampon alloué par tas, ainsi que des variables supplémentaires permettant de suivre la taille et la capacité du vecteur.


Il semble donc que lorsque je Push_back() passe au vecteur numbers, ses éléments les plus anciens changent d’emplacement.

La mémoire tampon allouée au tas a une capacité fixe. Lorsque vous atteignez la fin du tampon, un nouveau tampon sera alloué quelque part sur le tas et tous les éléments précédents seront déplacés dans le nouveau. . Leurs adresses vont donc changer.


Est-ce que cela les stocke peut-être ensemble, mais les déplace-t-il quand plus d'espace est nécessaire?

Grosso modo, oui. La stabilité des éléments avec les itérateurs et les adresses est garantie avec std::vector uniquement si aucune réallocation n’a lieu.


Je suis conscient que std::vector Est un conteneur contigu seulement depuis C++ 17

La structure de la mémoire de std::vector N'a pas changé depuis sa première apparition dans la norme. ContiguousContainer est simplement un "concept" qui a été ajouté pour différencier les conteneurs contigus des autres au moment de la compilation.

55
Vittorio Romeo

La réponse

C'est un seul stockage contigu (un tableau 1d). Chaque fois qu'il manque de capacité, il est réaffecté et les objets stockés sont déplacés vers le nouvel emplacement plus grand. C'est pourquoi vous observez que les adresses des objets stockés changent.

Cela a toujours été comme ça, pas depuis C++17.

TL; DR

Le stockage s'agrandit géométriquement pour garantir l'exigence de l'amorti O(1)Push_back(). Le facteur de croissance est 2 (Casquetten + 1= Casquetten+ Casquetten) dans la plupart des implémentations de la bibliothèque standard C++ ( GCC , Clang , STLPort ) et 1.5 (Casquetten + 1= Casquetten+ Casquetten/ 2) dans la variante MSVC .

growing std::vector

Si vous pré-allouez-le avec vector::reserve(N) et une taille suffisamment grande N, les adresses des objets stockés ne seront pas modifiées lorsque vous en ajouterez de nouveaux.

Dans la plupart des applications pratiques, il vaut généralement la peine de le pré-allouer à au moins 32 éléments pour éviter les premières réaffectations qui se succèdent (0 → 1 → 2 → 4 → 8 → 16).

Il est aussi parfois pratique de le ralentir, de passer à la politique de croissance arithmétique (Casquetten + 1= Casquetten+ Const), ou arrêtez-vous complètement après une taille assez grande pour vous assurer que l’application ne gaspille pas et ne croît pas en mémoire.

Enfin, dans certaines applications pratiques, telles que les stockages d’objets à base de colonnes, il peut être utile d’abandonner complètement l’idée de stockage contigu au profit d’un stockage segmenté (comme ce que fait std::deque Mais avec des morceaux beaucoup plus volumineux). De cette façon, les données peuvent être stockées de manière raisonnablement bien localisée pour les requêtes par colonne et par ligne (bien que cela puisse également nécessiter l’aide de l’allocateur de mémoire).

13
bobah

std::vector être un conteneur contigu signifie exactement ce que vous pensez que cela signifie.

Cependant, de nombreuses opérations sur un vecteur peuvent repositionner l'intégralité de la mémoire.

Un cas courant est lorsque vous ajoutez un élément à celui-ci, le vecteur doit croître, il peut réaffecter et copier tous les éléments dans un autre morceau de mémoire contigu.

7
nos

Alors qu'est-ce que cela signifie exactement, que std :: vector est un conteneur contigu et pourquoi ses éléments bougent-ils? Est-ce que cela les stocke peut-être ensemble, mais les déplace-t-il quand plus d'espace est nécessaire?

C'est exactement comment cela fonctionne et pourquoi l'ajout d'éléments invalide en effet tous les itérateurs ainsi que les emplacements de mémoire lorsqu'une réallocation a lieu¹. Ce n'est pas seulement valable depuis C++ 17, c'est le cas depuis.

Cette approche présente plusieurs avantages:

  • C'est très convivial en cache et donc efficace.
  • La méthode data() peut être utilisée pour transmettre la mémoire brute sous-jacente aux API qui fonctionnent avec des pointeurs bruts.
  • Le coût d’allocation de nouvelle mémoire selon Push_back, reserve ou resize se réduit à temps constant, car la croissance géométrique s’amortit avec le temps (chaque fois que Push_back appelé la capacité est doublée dans libc ++ et libstdc ++, et une croissance environ d’un facteur 1,5 dans MSVC).
  • Il permet la catégorie d’itérateurs la plus restreinte, c’est-à-dire les itérateurs à accès aléatoire, car l’arithmétique classique des pointeurs s’arrange bien lorsque les données sont stockées de manière contiguë.
  • Déplacer la construction d'une instance vectorielle à partir d'une autre est très économique.

Ces implications peuvent être considérées comme les inconvénients d’une telle configuration mémoire:

  • Tous les itérateurs et les pointeurs sur des éléments sont invalidés lors de modifications du vecteur qui impliquent une réallocation. Cela peut conduire à des bugs subtils lorsque, par exemple, effacer des éléments en parcourant les éléments d’un vecteur.
  • Des opérations telles que Push_front (Comme std::list Ou std::deque Fournir) ne sont pas fournies (insert(vec.begin(), element) fonctionne, mais est peut-être coûteuse¹), ainsi qu'une fusion efficace/épissage de plusieurs instances vectorielles.

¹ Merci à @ FrançoisAndrieux de l'avoir signalé.

5
lubgr

En termes de structure réelle, un std::vector ressemble à ceci en mémoire:

struct vector {    // Simple C struct as example (T is the type supplied by the template)
  T *begin;        // vector::begin() probably returns this value
  T *end;          // vector::end() probably returns this value
  T *end_capacity; // First non-valid address
  // Allocator state might be stored here (most allocators are stateless)
};

extrait de code pertinent de la libc++ implémentation utilisée par LLVM

Imprimer le contenu brut de la mémoire d'un std::vector:
(Ne faites pas ceci si vous ne savez pas ce que vous faites!)

#include <iostream>
#include <vector>

struct vector {
    int *begin;
    int *end;
    int *end_capacity;
};

int main() {
    union vecunion {
        std::vector<int> stdvec;
        vector           myvec;
        ~vecunion() { /* do nothing */ }
    } vec = { std::vector<int>() };
    union veciterator {
        std::vector<int>::iterator stditer;
        int                       *myiter;
        ~veciterator() { /* do nothing */ }
    };

    vec.stdvec.Push_back(1); // Add something so we don't have an empty vector

    std::cout
      << "vec.begin          = " << vec.myvec.begin << "\n"
      << "vec.end            = " << vec.myvec.end << "\n"
      << "vec.end_capacity   = " << vec.myvec.end_capacity << "\n"
      << "vec's size         = " << vec.myvec.end - vec.myvec.begin << "\n"
      << "vec's capacity     = " << vec.myvec.end_capacity - vec.myvec.begin << "\n"
      << "vector::begin()    = " << (veciterator { vec.stdvec.begin() }).myiter << "\n"
      << "vector::end()      = " << (veciterator { vec.stdvec.end()   }).myiter << "\n"
      << "vector::size()     = " << vec.stdvec.size() << "\n"
      << "vector::capacity() = " << vec.stdvec.capacity() << "\n"
      ;
}
1
YoYoYonnY