A 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, pas éparpillés dans la mémoire. J'ai simplement accepté le fait et utilisé cette connaissance quand par exemple en utilisant son data() méthode pour obtenir le morceau contigu de mémoire sous-jacent.

cependant, je suis tombé sur une situation, où la mémoire du vecteur se comporte d'une 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 nombres et un vecteur de pointeurs vers ces numéros. Cependant, lors de la liste du contenu du ptr_numbers pointeurs, il y a 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 de 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 comme ceci:

1

some random number
2

some random number
some random number
3

il semble donc comme si quand je push_back()numbers vecteur, ses éléments plus anciens changent leur emplacement.

Alors, quel est-il dire exactement, que std::vector est un conteneur contigu et pourquoi ses éléments se déplacent-ils? Est-ce qu'il peut les stocker ensemble, mais les déplace tous ensemble, quand plus d'espace est nécessaire?

Edit: Is std::vector contigu seulement depuis c++17? (Juste pour garder les commentaires sur mon revendication précédente, pour les futurs lecteurs.)

35
demandé sur McSim 2018-09-14 13:24:58

5 réponses

Il ressemble plus ou moins à ce (excusez mon MS Paint chef-d'œuvre):

vector memory layout

std::vector l'instance que vous avez sur la pile est un petit objet contenant un pointeur vers un tampon alloué en tas, plus quelques variables supplémentaires pour garder la trace de la taille et de la capacité du vecteur.


il semble donc comme si quand je push_back()numbers vectoriel, ses éléments les plus anciens changent leurs emplacement.

le tampon alloué par TAS a une capacité fixe. Lorsque vous atteignez la fin du tampon, a nouveau tampon sera alloué quelque part d'autre 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 ensemble, mais les déplace tous ensemble, quand plus d'espace est nécessaire?

en gros, oui. Iterator et la stabilité d'adresse des éléments est garantie avec std::vectorseulement si pas de réaffectation.


je suis conscient, que std::vector est un conteneur contigu seulement depuis C++17

La disposition de mémoire std::vector n'a pas changé depuis sa première apparition dans la Série. ContiguousContainer est juste un" concept " qui a été ajouté pour différencier les conteneurs contigus des autres lors de la compilation.

52
répondu Vittorio Romeo 2018-09-14 11:16:53

La Réponse

c'est un stockage contigu simple (un tableau 1d). Chaque fois qu'il est à court de capacité, il est réattribué et les objets stockés sont déplacés vers le nouvel endroit plus grand - c'est pourquoi vous observez les adresses des objets stockés changer.

Il a toujours été de cette façon, et non pas depuis C++17.

TL; DR

Le stockage est en croissance géométriquement pour s'assurer que l'exigence de l'amorti O(1)push_back(). Croissance le facteur est de 2 ( Cap n+1 = Cap n + Cap n) dans la plupart des implémentations de la bibliothèque Standard C++ (GCC, Clang, STLPort) et de 1,5 ( Cap n+1 = Cap n + Cap n / 2) MSVC variante.

growing std::vector

Si vous pré-allouer par vector::reserve(N) et suffisamment grand N, alors les adresses des objets stockés ne changera pas lorsque vous ajoutez de nouveaux.

dans la plupart des applications pratiques, il vaut généralement la peine de pré-affecter à au moins 32 éléments pour sauter les premières réaffectations peu de temps après l'Autre (0→1→2→4→8→16).

il est également parfois pratique de le ralentir, passer à la Politique de croissance arithmétique ( Cap n+1 = Cap n + Const), ou s'arrêter complètement après une certaine taille raisonnablement grande pour s'assurer que l'application ne gaspille pas ou ne sort pas de la mémoire.

enfin, dans certaines applications pratiques, comme les dépôts d'objets basés sur des colonnes, il peut être intéressant de renoncer à l'idée de stockage contigu complètement en faveur d'un stockage segmenté (identique à ce que std::deque mais avec des morceaux beaucoup plus grands). De cette façon, les données peuvent être stockées raisonnablement bien localisé pour les requêtes par colonne et par ligne (bien que cela puisse nécessiter de l'aide de l'allocateur mémoire).

11
répondu bobah 2018-09-15 11:03:01

std::vector être un conteneur contigu signifie exactement ce que vous pensez qu'il signifie.

cependant, de nombreuses opérations sur un vecteur peuvent re-localiser ce morceau entier de mémoire.

un cas commun est quand vous ajoutez un élément à elle, le vecteur doit croître, il peut réattribuer et copier tous les éléments à un autre morceau contigu de mémoire.

7
répondu nos 2018-09-14 10:29:11

alors qu'est-ce que cela signifie exactement, que std::vector est un conteneur contigu et pourquoi ses éléments se déplacent-ils? Est-ce qu'il peut les stocker ensemble, mais les déplace tous ensemble, quand plus d'espace est nécessaire?

c'est exactement comme ça que ça marche et pourquoi ajouter des éléments invalide en effet tous les itérateurs ainsi que les emplacements de mémoire quand une réallocation a lieu1. Ce n'est pas seulement valable depuis C++17, il a été le cas depuis.

il y a un quelques avantages de cette approche:

  • il est très facile à mettre en cache et donc efficace.
  • data() la méthode peut être utilisée pour passer la mémoire brute sous-jacente aux API qui fonctionnent avec des pointeurs bruts.
  • Le coût d'allocation de mémoire sur push_back,reserve ou resize se réduisent à temps constant, que la croissance géométrique amortit au fil du temps (à chaque fois push_back est appelé la capacité est doublée en libc++ et libstdc++, et environ. excroissances par un facteur de 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, parce que l'arithmétique Classique des pointeurs fonctionne bien lorsque les données sont stockées de façon contiguë.
  • déplacer la construction d'une instance vectorielle à partir d'une autre est très bon marché.

Ces implications peuvent être considérés comme l'inconvénient d'une telle disposition de la mémoire:

  • tous les itérateurs et les pointeurs vers les éléments sont invalidés modifications du vecteur impliquant une réallocation. Cela peut conduire à des bugs subtils lors de l'effacement d'éléments lors de l'itération des éléments d'un vecteur.
  • Activités push_front (std::list ou std::deque provide) ne sont pas fournis (insert(vec.begin(), element) fonctionne, mais est peut-être expensive1), ainsi que la fusion efficace/épissage des instances vectorielles multiples.

1 Merci à @FrançoisAndrieux de le souligner.

4
répondu lubgr 2018-09-14 11:13:19

en termes de structure réelle, un std::vector ressemble à quelque chose comme 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++ mise en œuvre utilisées par LLVM

imprimer le contenu de la mémoire brute d'un std::vector:

(Ne le faites pas 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
répondu YoYoYonnY 2018-09-14 18:11:44