5

私たちのプロジェクトの 1 つでメモリ リンクを調査しているときに、奇妙な問題に遭遇しました。どういうわけか、オブジェクトに割り当てられたメモリ (オブジェクトへの shared_ptr のベクトル、以下を参照) は、親コンテナーがスコープ外になり、小さなオブジェクトを除いて使用できない場合に完全に回収されません。

最小限の例: プログラムの開始時に、1.5Gb の単一の連続ブロックを問題なく割り当てることができます。(いくつかの小さなオブジェクトを作成および破棄することによって) メモリをある程度使用した後、大きなブロックの割り当てを行うことができなくなります。

テストプログラム:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class BigClass
{
private:
    double a[10000];
};

void TestMemory() {
    cout<< "Performing TestMemory"<<endl;
    vector<shared_ptr<BigClass>> list;
    for (int i = 0; i<10000; i++) {
        shared_ptr<BigClass> p(new BigClass());
        list.push_back(p);
    };
};

void TestBigBlock() {
    cout<< "Performing TestBigBlock"<<endl;
    char* bigBlock = new char [1024*1024*1536];
    delete[] bigBlock;
}

int main() {
    TestBigBlock();
    TestMemory();
    TestBigBlock();
}

また、shared_ptr の代わりに new/delete または malloc/free サイクルでプレーン ポインターを使用すると、問題が繰り返されます。

犯人は、TestMemory() の後、アプリケーションの仮想メモリが 827125760 のままであるようです (呼び出し回数に関係なく)。結果として、1.5 GB を保持するのに十分な大きさの空き VM 領域がありません。しかし、理由はわかりません。使用したメモリを確実に解放しているためです。OS呼び出しを最小限に抑えるためにCRTが行う「パフォーマンスの最適化」ですか?

環境は Windows 7 x64 + VS2012 + LAA なしの 32 ビット アプリです。

4

4 に答える 4

3

コメントできないため、さらに別の回答を投稿して申し訳ありません。他の多くの人が本当に答えにかなり近いと思います:-)

とにかく、犯人はおそらくアドレス空間の断片化です。Windows で Visual C++ を使用していると思います。

C / C++ ランタイム メモリ アロケータ (malloc または new によって呼び出される) は、Windows ヒープを使用してメモリを割り当てます。Windows ヒープ マネージャーには、アプリケーションが後で同様のサイズのブロックを要求した場合にそれらを再利用できるようにするために、特定のサイズ制限未満のブロックを保持する最適化があります。より大きなブロック (正確な値は覚えていませんが、約 1 メガバイトだと思います) の場合は、VirtualAlloc を完全に使用します。

多くの小さな割り当てのパターンを持つ他の長時間実行される 32 ビット アプリケーションにも、この問題があります。この問題を認識させたのはMATLABです。「セル配列」機能を使用して、基本的に何百万もの300〜400バイトのブロックを割り当てていたため、解放した後でもアドレス空間の断片化の問題が発生しました。

回避策として、Windows ヒープ関数 (HeapCreate() など) を使用してプライベート ヒープを作成し、それを介してメモリを割り当て (必要に応じてカスタム C++ アロケータをコンテナー クラスに渡します)、必要なときにそのヒープを破棄します。メモリバック - これには、ループ内の無数のブロックを delete() するのと比較して、非常に高速であるという嬉しい副作用もあります..

再。そもそも問題を引き起こす「メモリに残っているもの」:「メモリに」それ自体は何も残っていません。解放されたブロックがフリーとしてマークされているが、結合されていない場合です。ヒープ マネージャーにはアドレス空間のテーブル/マップがあり、空き領域を 1 つの連続したブロックに統合することを強制するものを割り当てることはできません (おそらくパフォーマンス ヒューリスティック)。

于 2014-04-10T13:37:08.230 に答える
1

ここには、本物の「リーク」を示すものは何もありません。あなたが説明する記憶のパターンは予想外のものではありません。ここでは、理解に役立ついくつかのポイントを示します。何が起こるかは、OS に大きく依存します。

  • プログラムには、多くの場合、長さを拡張または縮小できる単一のヒープがあります。ただし、これは 1 つの連続したメモリ領域であるため、サイズを変更すると、ヒープの末尾が変更されるだけです。これにより、OS にメモリを「戻す」ことが非常に困難になります。そのスペースに小さな小さなオブジェクトが 1 つでもあると、メモリの縮小が妨げられるからです。Linux では、関数 'brk' を検索できます (Windows を使用していることは知っていますが、同様のことを行うと思います)。

  • 多くの場合、大規模な割り当ては別の戦略で行われます。それらを汎用ヒープに入れるのではなく、追加のメモリ ブロックが作成されます。削除されると、このメモリは実際にはOSに「返される」可能性があります。これは、何も使用されていないことが保証されているためです。

  • 未使用のメモリの大きなブロックは、多くのリソースを消費する傾向はありません。通常、メモリをもう使用していない場合は、ディスクにページングされる可能性があります。一部の API 関数がメモリを使用していると言っているからといって、実際にはかなりのリソースを消費していると思い込まないでください。

  • API は常にユーザーの考えを報告するとは限りません。さまざまな最適化と戦略により、特定の瞬間にシステムで使用中または使用可能なメモリの量を実際に判断できない場合があります。OS の詳しい情報がない限り、これらの値が何を意味するのかはわかりません。

最初の 2 つのポイントは、小さなブロックの束と 1 つの大きなブロックが異なるメモリ パターンになる理由を説明できます。後者の点は、リークを検出するこのアプローチが役に立たない理由を示しています。本物のオブジェクトベースの「リーク」を検出するには、通常、割り当てを追跡する専用のプロファイリング ツールが必要です。


たとえば、提供されているコードでは次のようになります。

  1. TestBigBlock は配列を割り当てて削除します。これは特別なメモリ ブロックを使用すると想定されるため、メモリは OS に返されます。
  2. TestMemory はすべての小さなオブジェクトのヒープを拡張し、ヒープを OS に返すことはありません。ここでは、ヒープはアプリケーションの観点からは完全に利用可能ですが、OS の観点からはアプリケーションに割り当てられます。
  3. TestBigBlock は、特別なメモリ ブロックを使用しますが、全体のメモリ空間をヒープと共有し、2 が完了した後に十分なメモリが残っていないため、失敗します。
于 2013-10-29T10:28:15.300 に答える