カスタムソリューションを優先してstd::allocator
を捨てる理由は何ですか?正確性、パフォーマンス、拡張性などのために絶対に必要な状況に遭遇しましたか?本当に賢い例はありますか?
カスタムアロケーターは、私があまり必要としなかった標準ライブラリの機能でした。ここにいるSOの誰かが、彼らの存在を正当化する説得力のある例を提供できるのではないかと思っていました。
here に言及したように、Intel TBBのカスタムSTLアロケーターは、単一の
std::vector<T>
に
std::vector<T,tbb::scalable_allocator<T> >
(これは、TBBの気の利いたスレッドプライベートヒープを使用するようにアロケータを切り替える迅速かつ便利な方法です。 このドキュメントの7ページ を参照してください)
カスタムアロケーターが役立つ可能性のある領域の1つは、特にゲームコンソールでのゲーム開発です。これらは、メモリが少なく、スワップがないためです。そのようなシステムでは、各サブシステムを厳密に制御して、重要でないシステムが重要なシステムからメモリを盗むことがないようにする必要があります。プールアロケーターのような他のものは、メモリの断片化を減らすのに役立ちます。このトピックに関する長く詳細なペーパーは、次の場所にあります。
私は、ベクターがメモリーマップファイルからメモリーを使用できるようにするmmap-allocatorに取り組んでいます。目標は、mmapによってマップされた仮想メモリに直接あるストレージを使用するベクターを持つことです。私たちの問題は、コピーのオーバーヘッドなしで本当に大きなファイル(> 10GB)のメモリへの読み込みを改善することです。したがって、このカスタムアロケーターが必要です。
これまでのところ、カスタムアロケータ(std :: allocatorから派生)のスケルトンがありますが、独自のアロケータを作成するのに良い出発点だと思います。このコードを自由に使用してください。
#include <memory>
#include <stdio.h>
namespace mmap_allocator_namespace
{
// See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
template <typename T>
class mmap_allocator: public std::allocator<T>
{
public:
typedef size_t size_type;
typedef T* pointer;
typedef const T* const_pointer;
template<typename _Tp1>
struct rebind
{
typedef mmap_allocator<_Tp1> other;
};
pointer allocate(size_type n, const void *hint=0)
{
fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
return std::allocator<T>::allocate(n, hint);
}
void deallocate(pointer p, size_type n)
{
fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
return std::allocator<T>::deallocate(p, n);
}
mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
template <class U>
mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
~mmap_allocator() throw() { }
};
}
これを使用するには、次のようにSTLコンテナを宣言します。
using namespace std;
using namespace mmap_allocator_namespace;
vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());
たとえば、メモリが割り当てられるたびにログを記録するために使用できます。必要なのは、再バインド構造体です。それ以外の場合、ベクトルコンテナーはスーパークラスのallocate/deallocateメソッドを使用します。
更新:メモリマッピングアロケーターが https://github.com/johannesthoma/mmap_allocator で利用可能になり、LGPLになりました。プロジェクトに自由に使用してください。
コードにc ++を使用するMySQLストレージエンジンを使用しています。 MySQLのメモリを奪い合うのではなく、カスタムアロケータを使用してMySQLメモリシステムを使用しています。これにより、「余分な」ではなく、MySQLが使用するように設定されたユーザーとしてメモリを使用していることを確認できます。
カスタムアロケータを使用して、ヒープの代わりにメモリプールを使用すると便利な場合があります。これは他の多くの例の1つです。
ほとんどの場合、これは確かに時期尚早な最適化です。ただし、特定のコンテキスト(組み込みデバイス、ゲームなど)では非常に便利です。
カスタムSTLアロケーターを使用してC++コードを記述したことはありませんが、C++で記述されたWebサーバーを想像できます。応答が生成されると、カスタムアロケーターはすべての一時データを一度に解放できます。
カスタムアロケーターのもう1つの可能な使用例(私が使用した)は、関数の動作が入力の一部に依存しないことを証明する単体テストを作成しています。カスタムアロケーターは、メモリ領域を任意のパターンで埋めることができます。
GPUまたは他のコプロセッサーを使用する場合、特別な方法でメインメモリにデータ構造を割り当てることが有益な場合があります。この特別な方法メモリの割り当ては、便利な方法でカスタムアロケータに実装できます。
アクセラレータを使用する場合、アクセラレータランタイムを介したカスタム割り当てが有益である理由は次のとおりです。
ここではカスタムアロケーターを使用しています。 around他のカスタムダイナミックメモリ管理が機能することもあります。
背景:malloc、calloc、free、および演算子newとdeleteのさまざまなバリアントのオーバーロードがあり、リンカーはSTLにこれらを喜んで使用させます。これにより、自動スモールオブジェクトプーリング、リーク検出、アロケートフィル、フリーフィル、セントリーによるパディングアロケーション、特定のアロケートのキャッシュラインアライメント、遅延フリーなどを実行できます。
問題は、組み込み環境で実行していることです。長期にわたって適切にリーク検出アカウンティングを実際に実行するのに十分なメモリがありません。少なくとも、標準のRAMにはありません。カスタム割り当て関数を介して、他の場所にRAMのヒープが利用可能です。
解決策:拡張ヒープを使用するカスタムアロケータを作成し、メモリリークトラッキングアーキテクチャの内部でonlyを使用してください。これにより、トラッカー自体の追跡が回避されます(さらに、追加のパッキング機能も少し提供されます。トラッカーノードのサイズはわかっています)。
同じ理由で、これを使用して関数コストプロファイリングデータを保持します。関数の呼び出しと戻りごとにエントリを作成するだけでなく、スレッドの切り替えを行うと、コストが高くなります。カスタムアロケーターは、より大きなデバッグメモリ領域に小さなアロケートを提供します。
私はカスタムアロケーターを使用して、プログラムの一部の割り当て/割り当て解除の数を数え、それがどれくらいかかるかを測定しています。これを実現する方法は他にもありますが、この方法は私にとって非常に便利です。コンテナのサブセットにのみカスタムアロケーターを使用できることは特に便利です。
1つの重要な状況:モジュール(EXE/DLL)の境界を越えて機能する必要があるコードを記述する場合、割り当てと削除を1つのモジュールでのみ行うことが不可欠です。
これに遭遇したのは、Windows上のプラグインアーキテクチャでした。たとえば、DLL境界を越えてstd :: stringを渡す場合、文字列の再割り当ては、DLLこれは異なる場合があります*。
*実際にこれよりも複雑です。CRTに動的にリンクしているように、これはとにかく動作するかもしれません。しかし、各DLLがCRTへの静的リンクを持っている場合、ファントムアロケーションエラーが継続的に発生する痛みの世界に向かっています。
私がこれらを使用した時間の一例は、非常にリソースに制約のある組み込みシステムでの作業でした。たとえば、2kのRAMが無料であり、プログラムがそのメモリの一部を使用する必要があるとします。スタック上にない場所に4〜5個のシーケンスを保存する必要があります。さらに、これらのものを保存する場所に対して非常に正確なアクセス権が必要です。これは、独自のアロケーターを記述したい状況です。デフォルトの実装ではメモリが断片化する可能性があります。十分なメモリがなく、プログラムを再起動できない場合、これは受け入れられない可能性があります。
私が取り組んでいたプロジェクトの1つは、低電力チップでAVR-GCCを使用することでした。最大長がわかっている可変長の8つのシーケンスを保存する必要がありました。 メモリ管理の標準ライブラリ実装 は、malloc/freeの薄いラッパーであり、割り当てられたメモリのすべてのブロックの先頭に、割り当てられたメモリの終わりを過ぎた位置へのポインタを追加することで、アイテムの配置場所を追跡しますメモリの一部。新しいメモリを割り当てる場合、標準のアロケータは、メモリの各部分を調べて、要求されたサイズのメモリが収まる利用可能な次のブロックを見つける必要があります。デスクトッププラットフォームでは、このいくつかのアイテムの場合、これは非常に高速になりますが、これらのマイクロコントローラーのいくつかは、比較すると非常に遅く、原始的であることに留意する必要があります。さらに、メモリの断片化の問題は大規模な問題であったため、実際には別のアプローチを取るしかありませんでした。
そのため、独自の メモリプール を実装しました。メモリの各ブロックは、必要な最大のシーケンスに適合するのに十分な大きさでした。これにより、固定サイズのメモリブロックが事前に割り当てられ、現在使用されているメモリブロックがマークされました。これは、特定のブロックが使用されている場合に各ビットが表す1つの8ビット整数を保持することで実現しました。ここでは、プロセス全体を高速化するためにメモリ使用量と引き換えになりましたが、この場合、このマイクロコントローラーチップを最大処理能力に近づけようとしていたので、それは正当化されました。
これらのプラットフォーム の場合によくあるように、シーケンスのメモリがメインRAMにない場合など、組み込みシステムのコンテキストで独自のカスタムアロケータを作成するのを見ることができる他の回数があります=。
Andrei AlexandrescuのCppCon 2015のアロケーターに関する講演への必須リンク:
https://www.youtube.com/watch?v=LIb3L4vKZ7
良いことは、それらを考案するだけで、あなたがそれらをどのように使用するかのアイデアを考えるようになることです:-)
共有メモリの場合、コンテナヘッドだけでなく、コンテナヘッドに含まれるデータも共有メモリに保存することが重要です。
Boost :: Interprocess のアロケーターが良い例です。ただし、 here を読むと、すべてのSTLコンテナが共有メモリと互換性を持たせるには不十分です(異なるプロセスの異なるマッピングオフセットにより、ポインターが「壊れる」可能性があります)。
しばらく前に、このソリューションが非常に役立つことがわかりました: STLコンテナ用の高速C++ 11アロケーター 。 VS2017(〜5x)およびGCC(〜7x)でSTLコンテナをわずかに高速化します。これは、メモリプールに基づく特別な目的のアロケーターです。あなたが求めているメカニズムのおかげでのみ、STLコンテナで使用できます。
グラフィックシミュレーションでは、カスタムアロケーターが
std::allocator
が直接サポートしなかったアライメント制約。私は個人的にLoki :: Allocator/SmallObjectを使用して、小さなオブジェクトのメモリ使用量を最適化します。中程度の量の本当に小さなオブジェクト(1〜256バイト)で作業する必要がある場合、効率と満足のいくパフォーマンスを示します。多くの異なるサイズの中程度の量の小さなオブジェクトの割り当てについて話す場合、標準のC++新規/削除の割り当てよりも最大で30倍まで効率的です。また、「QuickHeap」と呼ばれるVC固有のソリューションがあり、最高のパフォーマンスをもたらします(割り当ておよび割り当て解除操作は、最大99でそれぞれヒープに割り当て/返されるブロックのアドレスを読み書きするだけです。(9)%ケース—設定と初期化に依存しますが、顕著なオーバーヘッドを犠牲にして—エクステントごとに2つのポインターと、新しいメモリブロックごとに1つの余分なポインターが必要です。さまざまなオブジェクトサイズが必要ない場合に作成および削除される膨大な(10,000個以上の)オブジェクトを操作するための最速のソリューションです(1〜1023バイトのオブジェクトサイズごとに個別のプールを作成します)現在の実装では、初期化コストは全体的なパフォーマンスの向上をわずかに抑える可能性がありますが、アプリケーションがパフォーマンスクリティカルなフェーズに入る前にいくつかのダミーオブジェクトを割り当て/割り当て解除できます。
標準のC++ new/delete実装の問題は、通常はC malloc/free割り当ての単なるラッパーであり、1024 +バイトなどの大きなメモリブロックに対して適切に機能することです。パフォーマンスの面で顕著なオーバーヘッドがあり、場合によってはマッピングにも余分なメモリが使用されます。そのため、ほとんどの場合、カスタムアロケータは、パフォーマンスを最大化する方法、および/または小さい(≤1024バイト)オブジェクトを割り当てるために必要な追加メモリの量を最小化する方法で実装されます。