6

によって報告されたメモリ使用量 (RES) に関して、Linux の興味深い動作に気付きましたtop。ヒープに数百万のオブジェクトを割り当てる次のプログラムを添付しました。各オブジェクトには約 1 キロバイトのバッファーがあります。std::listこれらのオブジェクトへのポインタは、 またはによって追跡されますstd::vector。私が気付いた興味深い動作は、 を使用するstd::listと、 によって報告されるメモリ使用量topがスリープ期間中に変化しないことです。ただし、 を使用するstd::vectorと、スリープ中にメモリ使用量が 0 近くまで低下します。

私のテスト構成は次のとおりです。
Fedora Core 16
Kernel 3.6.7-4
g++ バージョン 4.6.3

私がすでに知っていること:
1. std::vector は、必要に応じて (サイズを 2 倍にして) 再割り当てします。
2. std::list (私は信じています) は一度に 1 つの要素を割り当ててい
ます 3. std::vector と std::list の両方がデフォルトで std::allocator を使用して実際のメモリを取得しています
4. プログラムはリークしていません; valgrind は、リークの可能性はないと宣言しています。

私が混乱していること:
1. std::vector と std::list の両方が std::allocator を使用しています。std::vector がバッチ再割り当てを行っている場合でも、std::allocator はほぼ同じ配置で std::list と std::vector にメモリを配っていませんか? 結局、このプログラムはシングルスレッドです。
2. Linux のメモリ割り当ての動作についてどこで知ることができますか? Linux がプロセスを解放した後も RAM をプロセスに割り当てたままにしているという意見を聞いたことがありますが、その動作が保証されているかどうかはわかりません。std::vector を使用すると、その動作に大きな影響を与えるのはなぜですか?

これを読んでくれてありがとう。私はこれがかなりあいまいな問題であることを知っています。ここで探している「答え」は、この動作が「定義」されているかどうか、およびそのドキュメントを見つけることができる場所です。

#include <string.h>
#include <unistd.h>
#include <iostream>
#include <vector>
#include <list>
#include <iostream>
#include <memory>

class Foo{
public:
    Foo()
    {
        data = new char[999];
        memset(data, 'x', 999);
    }

    ~Foo()
    {
        delete[] data;
    }

private:
    char* data;

};

int main(int argc, char** argv)
{
    for(int x=0; x<10; ++x)
    {
        sleep(1);
        //std::auto_ptr<std::list<Foo*> > foos(new std::list<Foo*>);
        std::auto_ptr<std::vector<Foo*> > foos(new std::vector<Foo*>);
        for(int i=0; i<2000000; ++i)
        {
            foos->push_back(new Foo());
        }
        std::cout << "Sleeping before de-alloc\n";
        sleep(5);
        while(false == foos->empty())
        {
            delete foos->back();
            foos->pop_back();
        }
    }
    std::cout << "Sleeping after final de-alloc\n";
    sleep(5);
}
4

4 に答える 4

3

メモリの解放は「チャンク」ベースで行われます。listを使用すると、メモリが小さなビットに断片化される可能性があります。

ベクトルを使用して割り当てる場合、すべての要素が1つの大きなチャンクに格納されるため、メモリ解放コードは「ゴリー、ここに非常に大きな空き領域があるので、解放してOS」。ベクターを拡張するときに、メモリアロケータが「ラージチャンクモード」に移行する可能性もあります。これは、「スモールチャンクモード」とは異なる割り当て方法を使用します。たとえば、1 MBを超える割り当てを行うと、メモリ割り当てコードで次のように表示される場合があります。別の戦略を使い始める良い機会として、OSに「完璧にフィットする」メモリを要求してください。この大きなブロックは、解放されたときにOSにリリースするのが非常に簡単です。

一方、リストに追加する場合は、常に少しずつ要求しているため、アロケータは、大きなブロックを要求してから小さな部分を配るという別の戦略を使用します。チャンク内のすべてのブロックが解放されていることを確認するのは困難で時間がかかるため、アロケータは「気にしない」可能性があります。とにかく解放されることはありません。

また、メモリ測定として「top」を使用することは、特に正確な方法ではなく、OSとランタイムライブラリの動作に大きく依存するため、非常に信頼性が低いことも付け加えておきます。プロセスに属するメモリは「常駐」していない可能性がありますが、プロセスはまだそれを解放していません。「実際のメモリに存在する」だけではありません(代わりにスワップパーティションに存在します!)

そして、「これはどこかで定義されているか」という質問に対して、C /C++ライブラリのソースタラがそれを定義しているという意味だと思います。しかし、それはどこかに「これが機能することを意味する方法であり、私たちは決してそれを吊るさないことを約束する」と書かれているという意味で定義されていません。glibcおよびlibstdc++として提供されているライブラリは、新しいテクノロジーやアイデアが発明されると、mallocの内部を変更し、無料、新規、削除します。シナリオ。

コメントで指摘されているように、メモリはプロセスにロックされていません。カーネルがメモリが他の何かのためによりよく使用されていると感じた場合(そしてカーネルはここでは全能です)、実行中のプロセスからメモリを「盗み」、別のプロセスに渡します。特に長い間「触れられていない」記憶。

于 2012-12-31T22:43:30.310 に答える
2

1. std::vector と std::list の両方が std::allocator を使用しています。std::vector がバッチ再割り当てを行っている場合でも、std::allocator はほぼ同じ配置で std::list と std::vector にメモリを配っていませんか? 結局、このプログラムはシングルスレッドです。

さて、違いは何ですか?

  • std::listノードを 1 つずつ割り当てます (各ノードには、 に加えて 2 つのポインターが必要Foo *です)。また、これらのノードを再割り当てすることはありません (これは、 のイテレータ無効化要件によって保証されていますlist)。そのため、std::allocatorは基礎となるメカニズムから一連の固定サイズのチャンクを要求します (おそらく、システム コールまたはシステム コールmallocを使用します)。これらの固定サイズのチャンクは、リスト ノードよりも大きくなる可能性がありますが、その場合、すべてが で使用されるデフォルトのチャンク サイズと同じになります。sbrkmmapstd::allocator

  • std::vectorブックキーピングのオーバーヘッドなしで連続するポインターのブロックを割り当てます (これはすべてベクトルの親オブジェクトにあります)。a が現在の割り当てをオーバーフローするたびpush_backに、ベクターは新しい大きなチャンクを割り当て、すべてを新しいチャンクに移動し、古いチャンクを解放します。これで、新しいチャンクは古いチャンクの 2 倍 (または 1.6 倍など) のサイズになりますpush_back。したがって、すぐに、それが要求するサイズが の適切なデフォルトのチャンク サイズを超えると予想されstd::allocatorます。

したがって、興味深い相互作用は異なります。1 つはstd::vectorとアロケータの基礎となるメカニズムの間、もう 1 つはstd::allocatorそれ自体とその基礎となるメカニズムの間です。

2. Linux のメモリ割り当ての動作についてどこで学べますか。Linux がプロセスを解放した後も RAM をプロセスに割り当てたままにしているという意見を聞いたことがありますが、その動作が保証されているかどうかはわかりません。std::vector を使用すると、その動作に大きな影響を与えるのはなぜですか?

あなたが気にするかもしれないいくつかのレベルがあります:

  1. コンテナー独自の割り当てパターン: 上記で説明されていることを願っています
    • 実際のアプリケーションでは、コンテナの使用方法も同様に重要であることに注意してください
  2. std::allocatorそれ自体、小さな割り当てにバッファリングのレイヤーを提供する場合があります
    • これは標準で必要とは思わないので、実装に固有のものです
  3. 実装に依存する基礎となるアロケータ(たとえば、libc によって実装されてstd::allocatorいる可能性があります)malloc
  4. カーネルが使用する VM スキームと、syscall (3) が最終的に使用するものとの対話

あなたの特定のケースでは、リストよりも明らかに多くのメモリを解放するベクトルの可能な説明を考えることができます。

vector が 1 つの連続した割り当てで終了し、多くのFoos も連続して割り当てられることを考慮してください。つまり、このメモリをすべて解放すると、基になるページのほとんどが本当に空いていることが簡単にわかります。

ここで、リスト ノードの割り当てがインスタンスと 1:1 でインターリーブされているとしFooます。アロケーターが何らかのバッチ処理を行ったとしても、ヒープはその場合よりもはるかに断片化されている可能性がありますstd::vector。したがって、割り当てられたレコードを解放するときは、基礎となるページが現在解放されているかどうかを確認するために何らかの作業が必要であり、これが発生することを予期する特別な理由はありません (その後の大規模な割り当てによってヒープ レコードの合体が促進されない限り)。

于 2013-01-01T00:03:35.893 に答える
0

小さいチャンクはbrkを通過し、データセグメントと一定の分割と融合を調整し、大きいチャンクはプロセスを少し邪魔しません。詳細(PDF

また、ptmallocソースコード。

于 2013-01-01T00:54:17.600 に答える