カスタムC ++アロケーターの説得力のある例?


176

std::allocatorカスタムソリューションを採用する理由は何ですか?正確性、パフォーマンス、スケーラビリティなどのために絶対に必要な状況に遭遇しましたか?本当に賢い例はありますか?

カスタムアロケーターは、常に標準ライブラリの機能であり、あまり必要がありませんでした。ここのSOの誰かが彼らの存在を正当化するための説得力のある例を提供できるかどうか疑問に思っていました。

回答:


121

ここで述べたように、インテルTBBのカスタムSTLアロケーターは、シングルスレッドを変更するだけでマルチスレッドアプリのパフォーマンスを大幅に向上させるのを見てきました

std::vector<T>

std::vector<T,tbb::scalable_allocator<T> >

(これは、TBBの気の利いたスレッドプライベートヒープを使用するようにアロケータを切り替える便利な方法です。このドキュメントの7ページを参照してください


3
その2番目のリンクをありがとう。スレッドプライベートヒープを実装するためのアロケーターの使用は賢明です。これは、リソースが制限されていない(埋め込みまたはコンソール)シナリオでカスタムアロケーターが明確な利点を発揮する良い例であることを気に入っています。
Naaff、2009

7
元のリンクは現在無効ですが、CiteSeerにはPDFがあります:citeseerx.ist.psu.edu/viewdoc/summary?doi
Arto Bendiken

1
私は尋ねなければなりません:そのようなベクトルを別のスレッドに確実に移動できますか?(私はそうは思いません)
セルビッツェ

@sellibitze:ベクトルはTBBタスク内から操作され、複数の並列操作で再利用されていたため、どのTBBワーカースレッドがタスクを取得するかは保証されていないため、うまく機能すると結論付けています。ただし、あるスレッドで別のスレッドで作成されたTBB解放のものにはいくつかの歴史的な問題があります(スレッドのプライベートヒープとプロデューサー/コンシューマーパターンの割り当てと割り当て解除の古典的な問題と思われます。 。新しいバージョンで修正される可能性があります。)
timday '22

@ArtoBendiken:リンクのダウンロードリンクが有効ではないようです。
einpoklum

81

カスタムアロケーターが役立つ可能性がある1つの領域は、特にゲームコンソールでのゲーム開発です。これらはメモリが少なく、スワップがないためです。このようなシステムでは、重要でないシステムが重要なシステムからメモリを盗用できないように、各サブシステムを厳密に制御できるようにする必要があります。プールアロケーターのような他のものは、メモリの断片化を減らすのに役立ちます。このトピックに関する長くて詳細な論文は、次の場所にあります。

EASTL-Electronic Arts Standardテンプレートライブラリ


14
EASTLリンクの+1:「ゲーム開発者の中で、[STLの]最も根本的な弱点はstdアロケータの設計であり、この弱点がEASTLの作成に最大の要因であった」
ナフ2009

65

私は、ベクトルがメモリマップファイルのメモリを使用できるようにする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>());

たとえば、メモリが割り当てられたときにログを記録するために使用できます。必要なのは再バインド構造体です。それ以外の場合、ベクターコンテナーはスーパークラスの割り当て/割り当て解除メソッドを使用します。

更新:メモリマッピングアロケーターは、https://github.com/johannesthoma/mmap_allocatorから入手でき、LGPLです。プロジェクトに自由に使用してください。


17
std :: allocatorからの派生は、実際にはアロケータを記述する慣用的な方法ではありません。代わりにallocator_traitsを見る必要があります。これにより、最小限の機能を提供でき、traitsクラスが残りを提供します。STLは常に直接ではなくallocator_traitsを介してアロケーターを使用するため、自分でallocator_traitsを参照する必要はないことに注意してください。
Nir Friedman、2015年

25

コードにc ++を使用するMySQLストレージエンジンを使用しています。MySQLとメモリを競合するのではなく、カスタムアロケーターを使用してMySQLメモリシステムを使用しています。これにより、ユーザーがMySQLを使用するために設定したメモリとして、「余分」ではなくメモリを使用していることを確認できます。


21

ヒープの代わりにメモリプールを使用するには、カスタムアロケータを使用すると便利です。これは他の多くの例の1つです。

ほとんどの場合、これは確かに時期尚早の最適化です。ただし、特定の状況(組み込みデバイス、ゲームなど)では非常に役立ちます。


3
または、そのメモリプールが共有されている場合。
アンソニー

9

カスタムSTLアロケーターを使用してC ++コードを記述していませんが、HTTPリクエストへの応答に必要な一時データの自動削除にカスタムアロケーターを使用するC ++で記述されたWebサーバーを想像できます。カスタムアロケータは、応答が生成されると、すべての一時データを一度に解放できます。

カスタムアロケーター(私が使用したもの)のもう1つの可能なユースケースは、関数の動作が入力の一部に依存しないことを証明する単体テストを作成することです。カスタムアロケーターは、メモリ領域を任意のパターンで埋めることができます。


5
最初の例は、アロケータではなくデストラクタの仕事のようです。
マイケルドースト2014

2
ヒープからのメモリの初期内容に依存してプログラムが心配な場合は、valgrindですばやく(つまり、一晩で)実行すると、どちらか一方の方法で通知されます。
cdyson37 2015年

3
@anthropomorphic:デストラクタとカスタムアロケータは一緒に動作し、デストラクタが最初に実行され、次にカスタムアロケータが削除され、free(...)はまだ呼び出されませんが、free(...)が呼び出されます後で、リクエストの処理が終了したとき。これは、デフォルトのアロケーターよりも高速で、アドレス空間の断片化を減らすことができます。
2015年

8

GPUまたは他のコプロセッサーを使用する場合、特別な方法でデータ構造をメインメモリに割り当てると効果的な場合があります。メモリを割り当てるこの特別な方法は、便利な方法でカスタムアロケーターに実装できます。

アクセラレータを使用するときに、アクセラレータランタイムによるカスタム割り当てが有益である理由は次のとおりです。

  1. カスタム割り当てにより、アクセラレータランタイムまたはドライバーにメモリブロックが通知されます。
  2. さらに、オペレーティングシステムは、割り当てられたメモリブロックがページロックされていることを確認できます(これは、このピン留めメモリと呼ばれることもあります)。つまり、オペレーティングシステムの仮想メモリサブシステムは、メモリ内またはメモリからページを移動または削除できません。
  3. 1.と2.が保持され、ページロックされたメモリブロックとアクセラレータ間のデータ転送が要求された場合、ランタイムはメインメモリ内のデータに直接アクセスできます。移動/削除
  4. これにより、ページロックされていない方法で割り当てられたメモリで発生するメモリコピーが1つ節約されます。アクセラレータを使用して、メインメモリのデータをページロックされたステージング領域にコピーする必要があります。 )

1
...ページに揃えられたメモリブロックを忘れないでください。これは、ドライバーと(つまり、DMAを介してFPGAを使用して)通信していて、DMAスキャッタリストのページ内オフセットを計算する手間とオーバーヘッドが必要ない場合に特に便利です。
1

7

ここではカスタムアロケーターを使用しています。他のカスタムの動的メモリ管理を回避するためだったとさえ言うかもしれません。

背景:malloc、calloc、free、およびオペレーターnewおよびdeleteのさまざまなバリアントのオーバーロードがあり、リンカーはSTLにこれらを使用させてくれます。これにより、自動小さなオブジェクトプーリング、リーク検出、割り当ての塗りつぶし、空き塗りつぶし、監視機能によるパディング割り当て、特定の割り当てのキャッシュラインアライメント、遅延解放などを実行できます。

問題は、組み込み環境で実行していることです。実際には、長期間にわたって適切にリーク検出アカウンティングを実行するのに十分なメモリがありません。少なくとも、標準のRAMにはありません-カスタムの割り当て関数を介して、他の場所に利用可能なRAMの別のヒープがあります。

解決策:拡張ヒープを使用するカスタムアロケーターを記述し、それのみを使用するメモリリーク追跡アーキテクチャの内部でます...それ以外はすべて、デフォルトで、リーク追跡を行う通常の新規/削除オーバーロードになります。これにより、トラッカー自体の追跡が回避されます(トラッカーノードのサイズがわかっているため、パッキング機能も少し追加されます)。

同じ理由で、これを使用して関数コストプロファイリングデータを保持します。各関数の呼び出しと戻り、およびスレッドの切り替えごとにエントリを書き込むと、コストがかかります。カスタムアロケータにより、デバッグメモリ領域が大きくなり、割り当てが小さくなります。


5

プログラムの一部で割り当て/割り当て解除の数をカウントし、その所要時間を測定するために、カスタムアロケーターを使用しています。これを実現する方法は他にもありますが、この方法は私にとって非常に便利です。コンテナーのサブセットのみにカスタムアロケーターを使用できることは特に便利です。


4

重要な状況の1つ:モジュール(EXE / DLL)の境界を越えて機能する必要のあるコードを作成する場合、割り当てと削除が1つのモジュールでのみ行われるようにすることが重要です。

私がこれに遭遇したのは、Windowsのプラグインアーキテクチャでした。たとえば、DLLの境界を越えてstd :: stringを渡した場合、文字列の再割り当ては、DLLのヒープではなく、元のヒープから行われることが重要です*。

* CRTに動的にリンクしているかのように動作する可能性があるため、実際にはこれよりも複雑です。ただし、各DLLにCRTへの静的リンクがある場合は、幻影の割り当てエラーが継続的に発生する痛みの世界に向かっています。


DLLの境界を越えてオブジェクトを渡す場合は、両側でマルチスレッド(デバッグ)DLL(/ MD(d))設定を使用する必要があります。C ++はモジュールのサポートを考慮して設計されていません。または、COMインターフェイスの背後にあるすべてのものをシールドして、CoTaskMemAllocを使用することもできます。これは、特定のコンパイラ、STL、ベンダーにバインドされていないプラグインインターフェイスを使用するための最良の方法です。
gast128 2016年

古い人はそれを支配します:それをしないでください。DLL APIではSTLタイプを使用しないでください。また、DLL APIの境界を越えて動的メモリ解放の責任を渡さないでください。C ++ ABIはありません。すべてのDLLをC APIとして扱う場合、潜在的な問題のクラス全体を回避できます。もちろん「c ++美」を犠牲にして。または、他のコメントが示唆するように、COMを使用します。単純なC ++は悪い考えです。
BitTickler

3

私がこれらを使用したときの1つの例は、非常にリソースに制約のある組み込みシステムでの作業でした。2kのRAMがなく、プログラムがそのメモリの一部を使用する必要があるとします。スタック上ではない場所に4〜5のシーケンスを格納する必要があります。さらに、これらのものが格納される場所を非常に正確にアクセスする必要があります。これは、独自のアロケーターを記述したい状況です。デフォルトの実装はメモリを断片化する可能性があり、十分なメモリがなく、プログラムを再起動できない場合、これは受け入れられない場合があります。

私が取り組んでいたプロジェクトの1つは、一部の低消費電力チップでAVR-GCCを使用することでした。可変長の8つのシーケンスを格納する必要がありましたが、既知の最大値がありました。メモリ管理標準ライブラリ実装malloc / freeの薄いラッパーで、割り当てられたすべてのメモリブロックの先頭に、割り当てられたメモリの最後をちょうど過ぎたところへのポインタを付けることで、アイテムの配置場所を追跡します。新しいメモリを割り当てるとき、標準アロケータはメモリの各部分をウォークスルーして、要求されたメモリサイズが収まる次の使用可能なブロックを見つける必要があります。デスクトッププラットフォームでは、このいくつかの項目ではこれは非常に高速ですが、これらのマイクロコントローラーの一部は比較すると非常に低速でプリミティブであることを覚えておく必要があります。さらに、メモリの断片化の問題は非常に大きな問題であり、実際には別のアプローチを取るしかありませんでした。

だから私たちがやったことは私たち自身のメモリプールを実装することでした。メモリの各ブロックは、必要な最大のシーケンスに適合するのに十分な大きさでした。これにより、固定サイズのメモリブロックが事前に割り当てられ、現在使用されているメモリブロックがマークされます。これは、特定のブロックが使用された場合に各ビットが表す1つの8ビット整数を維持することで実現しました。ここでは、プロセス全体を高速化するためにメモリ使用量をトレードオフしました。この場合、このマイクロコントローラーチップを最大処理能力に近づけたため、これは正当化されました。

組み込みシステムのコンテキストで独自のカスタムアロケーターを作成しているのを目にすることがあるのは他にもたくさんあります。たとえば、シーケンスのメモリがメインRAMにない場合は、 これらのプラットフォームでです



2

共有メモリの場合、コンテナヘッドだけでなく、そこに含まれるデータも共有メモリに格納されることが重要です。

Boost :: Interprocessのアロケーターが良い例です。ただし、ここで読むことができるように、すべてのSTLコンテナを共有メモリに互換性を持たせるには、これだけでは不十分です(プロセスごとに異なるマッピングオフセットがあるため、ポインタが「壊れる」可能性があります)。


2

いつか私はこのソリューションが非常に役立つことに気づきました:STLコンテナー用の高速C ++ 11アロケーター。VS2017(〜5x)とGCC(〜7x)でSTLコンテナーをわずかに高速化します。これは、メモリプールに基づく特別な目的のアロケータです。それはあなたが求めているメカニズムのおかげでのみ、STLコンテナーで使用できます。


1

私は個人的にLoki :: Allocator / SmallObjectを使用して小さなオブジェクトのメモリ使用量を最適化します。中程度の量の本当に小さなオブジェクト(1〜256バイト)で作業する必要がある場合、効率と満足のいくパフォーマンスを示します。多くの異なるサイズの適度な量の小さなオブジェクトを割り当てる場合、標準のC ++の新規/削除の割り当てよりも最大で約30倍効率的です。また、「QuickHeap」と呼ばれるVC固有のソリューションがあり、可能な限り最高のパフォーマンスを提供します(割り当てと割り当て解除の操作は、ヒープに割り当てられている/返されているブロックのアドレスの読み取りと書き込みをそれぞれ最大99まで行います。(9)%の場合—設定と初期化に依存します)が、かなりのオーバーヘッドを犠牲にして—エクステントごとに2つのポインターと、新しいメモリブロックごとに1つの追加が必要です。それ'

標準のC ++ new / delete実装の問題は、通常、C malloc / free割り当ての単なるラッパーであり、1024以上のバイトなど、より大きなメモリブロックでうまく機能することです。パフォーマンスの面で顕著なオーバーヘッドがあり、マッピングに使用される追加のメモリも時々あります。そのため、ほとんどの場合、カスタムアロケータは、パフォーマンスを最大化するか、小さい(1024バイト以下)オブジェクトを割り当てるために必要な追加のメモリ量を最小化する方法で実装されます。


1

グラフィックシミュレーションでは、カスタムアロケータが

  1. std::allocator直接サポートしていない線形制約。
  2. 存続期間の短い(このフレームのみ)と存続期間の長い割り当てに別々のプールを使用することにより、断片化を最小限に抑えます。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.