記憶管理のために記憶プールを使用することを理解しようとしていますが、それは非常に一般的なメカニズムであるように見えますが、それについてはあまりわかりません。
これについて私が知っているのは、「固定サイズのブロックアルゴリズムとも呼ばれるメモリプール」-Wikipediaごとであり、それらのチャンクを使用して、オブジェクトのメモリを割り当てることができます。
メモリプールについての標準的な仕様はありますか?
これがヒープでどのように機能するか、どのように実装できるか、そしてこれをどのように使用する必要があるかを知りたいですか?
C++ 11メモリプールの設計パターンに関するこの質問 から、次のように読みました。
まだ準備ができていない場合は、Boost.Poolを使用して自分自身をfaimilia化してください。 Boost documentationから:
プールとは何ですか?
プールの割り当ては、非常に高速なメモリ割り当てスキームですが、その使用は制限されています。プールアレイの詳細については、seg regated storageとも呼ばれます concepts conceptsおよび Simple Segregated Storage を参照してください。
彼の意味を理解することはできますが、それを使用する方法や、メモリプールがアプリケーションを実際に使用する方法、実際に使用する方法を理解するのに役立ちません。
メモリプールの使用方法を示す簡単な例が適用されます。
あらゆる種類の「プール」とは、実際に事前に取得/初期化したリソースであり、クライアントがリクエストするたびにオンザフライで割り当てられるのではなく、準備ができています。クライアントがそれらの使用を終了すると、リソースは破棄される代わりにプールに戻ります。
メモリプールは、基本的に、事前に割り当てられたメモリのみです(通常、大きなブロックで)。たとえば、4キロバイトのメモリを事前に割り当てることができます。クライアントが64バイトのメモリを要求するときは、そのメモリプール内の未使用の領域へのポインタをクライアントに渡し、必要なものを読み書きできるようにします。クライアントが完了したら、メモリのそのセクションを再び未使用としてマークすることができます。
アライメント、安全性、または未使用の(解放された)メモリをプールに戻さない基本的な例として:
class MemoryPool
{
public:
MemoryPool(): ptr(mem)
{
}
void* allocate(int mem_size)
{
assert((ptr + mem_size) <= (mem + sizeof mem) && "Pool exhausted!");
void* mem = ptr;
ptr += mem_size;
return mem;
}
private:
MemoryPool(const MemoryPool&);
MemoryPool& operator=(const MemoryPool&);
char mem[4096];
char* ptr;
};
...
{
MemoryPool pool;
// Allocate an instance of `Foo` into a chunk returned by the memory pool.
Foo* foo = new(pool.allocate(sizeof(Foo))) Foo;
...
// Invoke the dtor manually since we used placement new.
foo->~Foo();
}
これは事実上、スタックからメモリをプールするだけです。より高度な実装では、ブロックをチェーンし、いくつかの分岐を行って、メモリが不足しないようにブロックがいっぱいであるかどうかを確認し、ユニオンである固定サイズのチャンクを処理します(ノードは解放され、クライアントは使用時にメモリが使用されます)。それは間違いなくアライメントを処理する必要があります(最も簡単な方法は、メモリブロックを最大にアライメントし、各チャンクにパディングを追加して、後続のチャンクをアライメントすることです)。
より豪華なのは、バディアロケーター、スラブ、フィッティングアルゴリズムを適用するものなどです。アロケーターの実装は、データ構造とそれほど変わらないのですが、生のビットやバイトに深く入り込み、アライメントなどについて考える必要があり、 t内容を入れ替えます(使用中のメモリへの既存のポインターを無効にすることはできません)。データ構造のように、「これを行う」と言う黄金の標準は実際にはありません。それらにはさまざまな種類があり、それぞれに長所と短所がありますが、メモリ割り当てには特に人気のあるアルゴリズムがいくつかあります。
アロケータの実装は、メモリ管理が少し良く機能する方法に合わせるために、実際に多くのCおよびC++開発者に推奨するものです。要求されているメモリがそれらを使用するデータ構造にどのように接続するかを少し意識させることができ、新しいデータ構造を使用せずに最適化の機会のまったく新しい扉を開きます。また、通常はあまり効率的ではないリンクリストのようなデータ構造をより有用にし、ヒープのオーバーヘッドを回避するために不透明/抽象型の不透明度を下げる誘惑を減らすことができます。ただし、後ですべての負荷を後悔するためだけにすべてのカスタムアロケータをシューホーンにする最初の興奮がある場合があります(特に、興奮してスレッドの安全性や配置などの問題を忘れた場合)。そこでのんびりする価値があります。あらゆるマイクロ最適化と同様に、通常、個別に、後から、プロファイラーを手に入れて適用するのが最適です。
メモリプールの基本的な概念は、アプリケーションにメモリの大部分を割り当てることです。その後、O_Sからメモリを要求するためにプレーンnew
を使用する代わりに、以前のチャンクを返します。代わりにメモリを割り当てました。
これを機能させるには、メモリ使用量を自分で管理する必要があり、O/Sに依存することはできません。つまり、独自のバージョンのnew
とdelete
を実装し、独自のメモリプールの割り当て、解放、またはサイズ変更の可能性がある場合にのみ元のバージョンを使用する必要があります。
最初のアプローチは、メモリプールをカプセル化し、new
とdelete
のセマンティクスを実装するカスタムメソッドを提供する独自のクラスを定義することですが、事前に割り当てられたプールからメモリを取得します。このプールは、new
を使用して割り当てられ、任意のサイズのメモリ領域にすぎないことに注意してください。 new
/delete
のプールのバージョンは、respを返します。ポインタを取る。最も単純なバージョンは、おそらくCコードのようになります。
_void *MyPool::malloc(const size_t &size)
void MyPool::free(void *ptr)
_
これをテンプレートでペッパーにして、変換を自動的に追加できます。
_template <typename T>
T *MyClass::malloc();
template <typename T>
void MyClass::free(T *ptr);
_
コンパイラーはsizeof(T)
でmalloc()
を呼び出すことができるため、テンプレート引数のおかげで_size_t size
_引数を省略できることに注意してください。
単純なポインタを返すということは、隣接するメモリが利用可能な場合にのみプールが拡大し、その「境界」にあるプールメモリが使用されない場合にのみ縮小できることを意味します。具体的には、malloc関数が返したすべてのポインターが無効になるため、プールを再配置することはできません。
この制限を修正する方法は、ポインターをポインターに返すことです。つまり、単に_T**
_ではなく_T*
_を返します。これにより、ユーザーに面する部分は同じままで、基になるポインターを変更できます。偶然にも、それは「ハンドル」と呼ばれていたNeXT O/Sのために行われました。ハンドルの内容にアクセスするには、_(*handle)->method()
_または_(**handle).method()
_を呼び出す必要がありました。最終的に、Maf Vosburgは、演算子の優先順位を利用して_(*handle)->method()
_構文を取り除く疑似演算子を発明しました:handle[0]->method();
これは sprong operator と呼ばれていました。
この操作の利点は次のとおりです。1つ目は、new
とdelete
への一般的な呼び出しのオーバーヘッドを回避すること、2つ目は、メモリプールにより、メモリの連続したセグメントがアプリケーションで使用されるようにすることです。つまり、メモリの断片化が回避されるため、CPUキャッシュヒットが増加します。
したがって、基本的には、メモリプールは、複雑になる可能性のあるアプリケーションコードの欠点によって得られるスピードアップを提供します。しかし、繰り返しになりますが、 boost :: pool のように、実績があり、簡単に使用できるメモリプールの実装があります。
基本的に、メモリプールを使用すると、メモリを頻繁に割り当てたり解放したりするプログラムでメモリを割り当てるための費用の一部を回避できます。あなたがすることは、実行の最初に大きなメモリのチャンクを割り当て、時間的にオーバーラップしない異なる割り当てのために同じメモリを再利用することです。使用可能なメモリを追跡し、そのメモリを割り当てに使用するためのメカニズムが必要です。メモリを使い終わったら、解放するのではなく、再び使用可能としてマークします。
言い換えると、new
/malloc
およびdelete
/free
を呼び出す代わりに、ユーザー定義のアロケータ/デアロケータ関数を呼び出します。
これを実行すると、実行中に1つの割り当てのみを行うことができます(必要なメモリの合計量がおおよそであると想定している場合)。プログラムがメモリに拘束されるのではなく、レイテンシである場合、メモリ使用量を犠牲にして、malloc
よりも高速に実行する割り当て関数を作成できます。