web-dev-qa-db-ja.com

メモリ内でstd :: vectorはどのように見えますか?

std::vectorは連続している必要があると読みました。私の理解では、その要素はメモリ全体に分散するのではなく、一緒に格納する必要があります。たとえば、data()メソッドを使用して基礎となる連続したメモリを取得するときに、事実を受け入れ、この知識を使用しました。

しかし、ベクターのメモリが奇妙な方法で動作する状況に遭遇しました。

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());
}

これにより、いくつかの数値のベクトルと、これらの数値へのポインターのベクトルが得られると期待していました。ただし、ptr_numbersポインターの内容を一覧表示するとき、メモリの間違った部分にアクセスしているように、異なる一見ランダムな数字があります。

私はすべてのステップで内容を確認しようとしました:

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;
}

結果はおおよそ次のようになります。

1

some random number
2

some random number
some random number
3

したがって、Push_back()numbersベクトルに変更すると、古い要素の位置が変わります。

std::vectorは連続したコンテナであり、その要素が移動するのはなぜですか?おそらく一緒に保存しますが、より多くのスペースが必要な場合は、それらをすべて一緒に移動しますか?

編集:std::vectorはC++ 17以降のみ連続していますか? (私の以前の主張に関するコメントを将来の読者に関連するようにするために。)

38
McSim

おおよそ次のようになります(私のMS Paintの傑作を言い訳)。

vector memory layout

スタックにあるstd::vectorインスタンスは、ヒープに割り当てられたバッファへのポインタと、ベクトルのサイズと容量を追跡するためのいくつかの追加変数を含む小さなオブジェクトです。


だから、Push_back()numbersベクトルにすると、古い要素が場所を変更するようです。

ヒープに割り当てられたバッファの容量は固定されています。バッファの最後に到達すると、新しいバッファがヒープ上のどこかに割り当てられ、以前のすべての要素が新しい要素に移動されます。したがって、アドレスは変更されます。


おそらく一緒に保存しますが、より多くのスペースが必要な場合は、それらをすべて一緒に移動しますか?

大体、はい。要素のイテレータとアドレスの安定性は、std::vectorで保証されますifif再割り当ては行われません。


std::vectorはC++ 17以降のみ連続したコンテナであることを認識しています

std::vectorのメモリレイアウトは、標準に初めて登場してから変更されていません。 ContiguousContainer は、コンパイル時に隣接するコンテナを他のコンテナと区別するために追加された単なる「概念」です。

55
Vittorio Romeo

答え

これは、単一の連続したストレージ(1次元配列)です。容量がなくなるたびに再割り当てされ、保存されたオブジェクトは新しい大きな場所に移動されます。これが、保存されたオブジェクトのアドレスが変化するのを観察する理由です。

C++17以降ではなく、常にこのようになっています。

TL; DR

ストレージは、償却済みO(1)Push_back()の要件を確保するために、幾何学的に成長しています。成長因子は2(キャップn + 1=キャップn+キャップn)C++標準ライブラリのほとんどの実装( GCCClangSTLPort )および1.5(キャップn + 1=キャップn+キャップn/ 2MSVC バリアント内。

growing std::vector

vector::reserve(N)と十分な大きさのNで事前に割り当てた場合、新しいオブジェクトを追加しても、保存されているオブジェクトのアドレスは変更されません。

ほとんどの実際のアプリケーションでは、通常、少なくとも32個の要素に事前に割り当てて、すぐ後に続く最初のいくつかの再割り当て(0→1→2→4→8→16)をスキップする価値があります。

速度を落とし、算術成長ポリシーに切り替えることも時々実用的です(キャップn + 1=キャップn+定数)、またはアプリケーションがメモリを浪費したりメモリを使い果たしたりしないように、ある程度大きなサイズの後に完全に停止します。

最後に、列ベースのオブジェクトストレージのようないくつかの実用的なアプリケーションでは、セグメント化されたストレージを完全に支持して連続ストレージのアイデアを放棄する価値があるかもしれません(std::dequeと同じですが、より大きなチャンクがあります)。この方法では、列ごとと行ごとの両方のクエリに対してローカライズされたデータが適切にローカライズされます(ただし、メモリアロケーターの助けが必要になる場合もあります)。

13
bobah

std::vectorが連続したコンテナであるということは、まさにあなたがそれが意味すると思うことを意味します。

ただし、ベクターに対する多くの操作では、メモリ全体を再配置できます。

一般的なケースの1つは、要素を追加すると、ベクトルが大きくなり、すべての要素を別の連続したメモリに再割り当てしてコピーできることです。

7
nos

それでは、std :: vectorは連続したコンテナであり、その要素はなぜ移動するのでしょうか?おそらく一緒に保存しますが、より多くのスペースが必要な場合は、それらをすべて一緒に移動しますか?

それがまさにその仕組みであり、要素の追加が実際にすべてのイテレータと再配置が行われたときにメモリの場所を無効にする理由です¹。これはC++ 17以降で有効であるだけでなく、それ以降も同様です。

このアプローチにはいくつかの利点があります。

  • 非常にキャッシュに優しいため、効率的です。
  • data()メソッドを使用して、基礎となる生メモリを生ポインタで動作するAPIに渡すことができます。
  • Push_backreserve、またはresizeに新しいメモリを割り当てるコストは、幾何学的な成長が時間とともに償却されるため、一定の時間になります(Push_backは容量と呼ばれるたびにlibc ++およびlibstdc ++では2倍になり、MSVCでは約1.5倍に増加します)。
  • データが連続して格納されている場合、従来のポインター演算がうまく機能するため、最も制限されたイテレーターカテゴリ、つまりランダムアクセスイテレーターが許可されます。
  • 別のインスタンスからベクトルインスタンスを作成するのは非常に安価です。

これらの影響は、このようなメモリレイアウトの欠点と考えることができます。

  • すべてのイテレータと要素へのポインタは、再割り当てを意味するベクトルの変更時に無効になります。これは、例えばベクトルの要素を反復しながら要素を消去します。
  • Push_frontstd::listまたはstd::dequeが提供)などの操作は提供されません(insert(vec.begin(), element)は機能しますが、高価になる可能性があります¹)、および複数のベクトルの効率的なマージ/スプライシングインスタンス。

¹それを指摘してくれた@FrançoisAndrieuxに感謝します。

5
lubgr

実際の構造に関しては、std::vectorはメモリ内で次のようになります。

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)
};

LLVMで使用されるlibc++実装の関連コードスニペット

std::vectorの未加工メモリの内容の印刷:
(何をしているのかわからない場合は、これをしないでください!)

#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