std::allocator
カスタムソリューションを支持するために捨てる本当に良い理由は何ですか?正確性、パフォーマンス、スケーラビリティなどに絶対に必要な状況に遭遇したことがありますか?本当に賢い例はありますか?
カスタムアロケータは、私があまり必要としなかった標準ライブラリの機能でした。ここSOの誰かが、彼らの存在を正当化するための説得力のある例を提供できるかどうか疑問に思っていました。
std::allocator
カスタムソリューションを支持するために捨てる本当に良い理由は何ですか?正確性、パフォーマンス、スケーラビリティなどに絶対に必要な状況に遭遇したことがありますか?本当に賢い例はありますか?
カスタムアロケータは、私があまり必要としなかった標準ライブラリの機能でした。ここSOの誰かが、彼らの存在を正当化するための説得力のある例を提供できるかどうか疑問に思っていました。
ここで述べたように、Intel TBBのカスタムSTLアロケータは、単一のアプリを変更するだけで、マルチスレッドアプリのパフォーマンスを大幅に向上させることがわかりました。
std::vector<T>
に
std::vector<T,tbb::scalable_allocator<T> >
(これは、TBBの気の利いたスレッドプライベートヒープを使用するようにアロケータを切り替えるための迅速で便利な方法です。このドキュメントの7ページを参照してください)
カスタムアロケータが役立つ可能性のある領域の1つは、特にゲームコンソールでのゲーム開発です。これは、メモリが少量でスワップがないためです。このようなシステムでは、各サブシステムを厳密に制御して、重要でない1つのシステムが重要なシステムからメモリを盗むことができないようにする必要があります。プールアロケータのような他のものは、メモリの断片化を減らすのに役立ちます。このトピックに関する長く詳細な論文は、次の場所にあります。
ベクトルがメモリマップトファイルのメモリを使用できるようにするmmapアロケータに取り組んでいます。目標は、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>());
たとえば、メモリが割り当てられるたびにログに記録するために使用できます。必要なのはrebind構造体です。そうでない場合、ベクターコンテナはスーパークラスのallocate/deallocateメソッドを使用します。
更新:メモリマッピングアロケータがhttps://github.com/johannesthoma/mmap_allocatorで利用可能になり、LGPLになりました。プロジェクトに自由に使用してください。
コードにc++を使用するMySQLストレージエンジンを使用しています。メモリについてMySQLと競合するのではなく、カスタムアロケータを使用してMySQLメモリシステムを使用しています。これにより、ユーザーが「余分な」ものではなく、使用するようにMySQLを構成したときにメモリを使用していることを確認できます。
カスタムアロケータを使用して、ヒープの代わりにメモリプールを使用すると便利な場合があります。これは、他の多くの例の1つです。
ほとんどの場合、これは確かに時期尚早の最適化です。ただし、特定のコンテキスト(組み込みデバイス、ゲームなど)では非常に便利です。
GPUまたは他のコプロセッサを使用する場合、特別な方法でメインメモリにデータ構造を割り当てることが有益な場合があります。メモリを割り当てるこの特別な方法は、便利な方法でカスタムアロケータに実装できます。
アクセラレータを使用するときにアクセラレータランタイムを介したカスタム割り当てが役立つ理由は次のとおりです。
カスタムSTLアロケータを使用してC++コードを記述したことはありませんが、HTTP要求への応答に必要な一時データの自動削除にカスタムアロケータを使用するC++で記述されたWebサーバーを想像できます。カスタムアロケータは、応答が生成されると、すべての一時データを一度に解放できます。
カスタムアロケータ(私が使用した)のもう1つの可能なユースケースは、関数の動作が入力の一部に依存しないことを証明する単体テストを作成することです。カスタムアロケータは、任意のパターンでメモリ領域を埋めることができます。
ここではカスタムアロケータを使用しています。他のカスタム動的メモリ管理を回避することだったとさえ言うかもしれません。
背景:malloc、calloc、free、および演算子newとdeleteのさまざまなバリアントのオーバーロードがあり、リンカーはSTLにこれらを喜んで使用させます。これにより、自動スモールオブジェクトプーリング、リーク検出、割り当ての塗りつぶし、空きの塗りつぶし、歩哨によるパディングの割り当て、特定の割り当てのキャッシュラインの調整、遅延のない解放などを行うことができます。
問題は、組み込み環境で実行していることです。長期間にわたってリーク検出アカウンティングを実際に適切に実行するための十分なメモリがありません。少なくとも、標準のRAMにはありません。カスタム割り当て機能を使用して、他の場所で利用できるRAMのヒープがもう1つあります。
解決策:拡張ヒープを使用するカスタムアロケータを作成し、それをメモリリークトラッキングアーキテクチャの内部でのみ使用します...他のすべては、リークトラッキングを行う通常のnew/deleteオーバーロードにデフォルト設定されます。これにより、トラッカーの追跡自体が回避されます(また、トラッカーノードのサイズがわかっているため、追加のパッキング機能も少し提供されます)。
同じ理由で、これを使用して関数コストプロファイリングデータを保持します。各関数呼び出しと戻り、およびスレッドスイッチのエントリを作成すると、コストが高くなる可能性があります。カスタムアロケータは、より大きなデバッグメモリ領域でより小さなアロケータを提供します。
プログラムの一部で割り当て/割り当て解除の数をカウントし、それにかかる時間を測定するために、カスタムアロケータを使用しています。これを実現する方法は他にもありますが、この方法は私にとって非常に便利です。カスタムアロケータをコンテナのサブセットにのみ使用できると特に便利です。
重要な状況の1つ:モジュール(EXE / DLL)の境界を越えて機能する必要のあるコードを作成する場合、割り当てと削除を1つのモジュールでのみ実行することが不可欠です。
私がこれに遭遇したのは、Windowsのプラグインアーキテクチャでした。たとえば、DLLの境界を越えてstd :: stringを渡す場合、文字列の再割り当ては、DLL内のヒープではなく、元のヒープから発生することが重要です*。
* CRTに動的にリンクしているように、これは実際にはこれよりも複雑です。これはとにかく機能する可能性があります。しかし、各DLLにCRTへの静的リンクがある場合、ファントム割り当てエラーが継続的に発生する苦痛の世界に向かっています。
アンドレイ・アレキサンドレスクのアロケータに関するCppCon 2015トークへの必須リンク:
https://www.youtube.com/watch?v=LIb3L4vKZ7U
良い点は、それらを考案するだけで、それらをどのように使用するかについてのアイデアを思いつくことができるということです:-)
カスタムアロケータは、メモリの割り当てを解除する前にメモリを安全に消去するための合理的な方法です。
template <class T>
class allocator
{
public:
using value_type = T;
allocator() noexcept {}
template <class U> allocator(allocator<U> const&) noexcept {}
value_type* // Use pointer if pointer is not a value_type*
allocate(std::size_t n)
{
return static_cast<value_type*>(::operator new (n*sizeof(value_type)));
}
void
deallocate(value_type* p, std::size_t) noexcept // Use pointer if pointer is not a value_type*
{
OPENSSL_cleanse(p, n);
::operator delete(p);
}
};
template <class T, class U>
bool
operator==(allocator<T> const&, allocator<U> const&) noexcept
{
return true;
}
template <class T, class U>
bool
operator!=(allocator<T> const& x, allocator<U> const& y) noexcept
{
return !(x == y);
}
Hinnantによるアロケーターボイラープレートの使用をお勧めします: https ://howardhinnant.github.io/allocator_boilerplate.html )
私がこれらを使用したときの1つの例は、非常にリソースに制約のある組み込みシステムでの作業でした。2kのRAMが空いていて、プログラムがそのメモリの一部を使用する必要があるとします。スタック上にない場所にたとえば4〜5のシーケンスを格納する必要があります。さらに、これらのものが格納される場所に非常に正確にアクセスできる必要があります。これは、独自のアロケータを作成する場合の状況です。デフォルトの実装ではメモリが断片化する可能性があります。十分なメモリがなく、プログラムを再起動できない場合、これは受け入れられない可能性があります。
私が取り組んでいたプロジェクトの1つは、一部の低電力チップでAVR-GCCを使用することでした。可変長の8つのシーケンスを格納する必要がありましたが、最大値は既知です。メモリ管理の標準ライブラリの実装はmalloc/freeの薄いラッパーであり、割り当てられたすべてのメモリブロックの前に、割り当てられたメモリの終わりを過ぎたところへのポインタを追加することで、アイテムを配置する場所を追跡します。新しいメモリを割り当てる場合、標準のアロケータは各メモリを調べて、要求されたサイズのメモリが収まる場所で使用可能な次のブロックを見つける必要があります。デスクトッププラットフォームでは、これはこのいくつかの項目では非常に高速ですが、これらのマイクロコントローラーのいくつかは、比較すると非常に低速で原始的であることに注意する必要があります。さらに、メモリの断片化の問題は大きな問題でした。つまり、別のアプローチを取るしかありませんでした。
したがって、私たちが行ったことは、独自のメモリプールを実装することでした。メモリの各ブロックは、必要な最大のシーケンスに適合するのに十分な大きさでした。これにより、固定サイズのメモリブロックが事前に割り当てられ、現在使用されているメモリブロックがマークされます。これを行うには、特定のブロックが使用された場合に各ビットが表される1つの8ビット整数を保持します。ここでは、プロセス全体を高速化するためにメモリ使用量をトレードオフしました。この場合、このマイクロコントローラチップを最大処理能力に近づけていたため、これは正当化されました。
組み込みシステムのコンテキストで独自のカスタムアロケータを作成しているのを見ることができる場合が他にもたくさんあります。たとえば、これらのプラットフォームでよくあるように、シーケンスのメモリがメインRAMにない場合です。
共有メモリの場合、コンテナヘッドだけでなく、コンテナヘッドに含まれるデータも共有メモリに保存することが重要です。
Boost::Interprocessのアロケータは良い例です。ただし、ここで読むことができるように、すべてのSTLコンテナを共有メモリ互換にするために、このすべてでは不十分です(プロセスごとにマッピングオフセットが異なるため、ポインタが「破損」する可能性があります)。
少し前に、このソリューションが私にとって非常に役立つことに気づきました。STLコンテナ用の高速C++11アロケータです。VS2017(〜5x)およびGCC(〜7x)でSTLコンテナーをわずかに高速化します。これは、メモリプールに基づく特別な目的のアロケータです。それはあなたが求めているメカニズムのおかげでのみSTLコンテナで使用することができます。
私は個人的にLoki::Allocator / SmallObjectを使用して、小さなオブジェクトのメモリ使用量を最適化します。適度な量の非常に小さなオブジェクト(1〜256バイト)で作業する必要がある場合は、優れた効率と満足のいくパフォーマンスを示します。多くの異なるサイズの小さなオブジェクトを適度に割り当てることについて話す場合、標準のC++の新規/削除の割り当てよりも最大30倍効率的です。また、「QuickHeap」と呼ばれるVC固有のソリューションがあり、可能な限り最高のパフォーマンスを実現します(最大99の場合、それぞれ、ヒープに割り当て/返されるブロックのアドレスを読み書きするだけの割り当ておよび割り当て解除操作) —設定と初期化によって異なります)が、かなりのオーバーヘッドが発生します—エクステントごとに2つのポインターが必要であり、新しいメモリブロックごとに1つの追加ポインターが必要です。それ'
標準のC++のnew/delete実装の問題は、通常、C malloc / free割り当てのラッパーであり、1024バイト以上などのより大きなメモリブロックに適していることです。パフォーマンスの点で顕著なオーバーヘッドがあり、場合によっては、マッピングにも追加のメモリが使用されます。したがって、ほとんどの場合、カスタムアロケータは、パフォーマンスを最大化する方法、および/または小さな(1024バイト以下)オブジェクトを割り当てるために必要な追加メモリの量を最小化する方法で実装されます。
グラフィックシミュレーションでは、カスタムアロケータが
std::allocator
直接サポートしなかった配置制約。