Kylotanの提案に似ていますが、可能な場合は下位のアロケーターレベルではなく、可能な場合はデータ構造レベルでこれを解決することをお勧めします。
以下はFoos
、要素が一緒にリンクされた穴を持つ配列を繰り返し使用して割り当てと解放を回避する方法の簡単な例です(これを「アロケーター」レベルではなく「コンテナー」レベルで解決します)。
struct FooNode
{
explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}
// Stores a 'Foo'.
Foo element;
// Points to the next foo available; either the
// next used foo or the next deleted foo. Can
// use SoA and hoist this out if Foo doesn't
// have 32-bit alignment.
int next;
};
struct Foos
{
// Stores all the Foo nodes.
vector<FooNode> nodes;
// Points to the first used node.
int first_node;
// Points to the first free node.
int free_node;
Foos(): first_node(-1), free_node(-1)
{
}
const FooNode& operator[](int n) const
{
return data[n];
}
void insert(const Foo& element)
{
int index = free_node;
if (index != -1)
{
// If there's a free node available,
// pop it from the free list, overwrite it,
// and push it to the used list.
free_node = data[index].next;
data[index].next = first_node;
data[index].element = element;
first_node = index;
}
else
{
// If there's no free node available, add a
// new node and push it to the used list.
FooNode new_node(element);
new_node.next = first_node;
first_node = data.size() - 1;
data.push_back(new_node);
}
}
void erase(int n)
{
// If the node being removed is the first used
// node, pop it from the used list.
if (first_node == n)
first_node = data[n].next;
// Push the node to the free list.
data[n].next = free_node;
free_node = n;
}
};
この効果のための何か:フリーリストを持つ一方向にリンクされたインデックスリスト。インデックスリンクを使用すると、削除された要素をスキップしたり、一定時間で要素を削除したり、一定時間挿入で空き要素を再利用/再利用/上書きしたりできます。構造を反復処理するには、次のようなことを行います。
for (int index = foos.first_node; index != -1; index = foos[index].next)
// do something with foos[index]
そして、テンプレートを使用して上記の種類の「ホールのリンク配列」データ構造を一般化し、コピー割り当ての要件を回避するための新規および手動のdtor呼び出しを配置し、要素が削除されたときにデストラクタを呼び出し、前方反復子を提供することができます。私は非常に怠け者であるため、概念をより明確に説明するために例を非常にC風に保つことを選択しました。
とは言っても、この構造は、物を中心から出し入れした後、空間的な局所性が低下する傾向があります。その時点で、next
リンクを使用してベクターに沿って前後に移動し、以前は同じシーケンシャルトラバース内でキャッシュラインから削除されたデータをリロードできます(これは、再利用中に要素をシャッフルせずに一定時間削除できるデータ構造またはアロケーターでは避けられません)一定の時間を挿入し、パラレルビットセットやremoved
フラグなどを使用せずに、中央からスペースを挿入します)。キャッシュの使いやすさを復元するには、次のようにコピーアクターとスワップメソッドを実装できます。
Foos(const Foos& other)
{
for (int index = other.first_node; index != -1; index = other[index].next)
insert(foos[index].element);
}
void Foos::swap(Foos& other)
{
nodes.swap(other.nodes):
std::swap(first_node, other.first_node);
std::swap(free_node, other.free_node);
}
// ... then just copy and swap:
Foos(foos).swap(foos);
これで、新しいバージョンは再びトラバースしやすくなりました。別の方法は、インデックスの個別のリストを構造に保存し、定期的にソートすることです。別の方法は、ビットセットを使用して、使用されるインデックスを示すことです。これにより、常にビットセットを順番にトラバースすることができます(これを効率的に行うには、たとえばFFS / FFZを使用して一度に64ビットをチェックします)。ビットセットは最も効率的で邪魔にならず、32ビットのnext
インデックスを必要とする代わりに、使用されるものと削除されるものを示すために要素ごとにパラレルビットのみを必要としますが、うまく書くのに最も時間がかかります(一度に1ビットをチェックしている場合は、トラバースを高速で実行します-占有インデックスの範囲を迅速に決定するには、FFS / FFZが一度に32ビット以上のセットまたはアンセットビットをすぐに見つける必要があります)。
このリンクされたソリューションは、一般的に実装が最も簡単で邪魔にならず(フラグFoo
を保存するために変更する必要はありませんremoved
)、32ビットを気にしないでこのコンテナを一般化して任意のデータ型で動作させたい場合に役立ちます要素ごとのオーバーヘッド。
ダイナミックアロケーション用のメモリプールを作成する必要がありますか、またはこれを気にする必要はありませんか?ターゲットプラットフォームがモバイルデバイスの場合はどうなりますか?
ニーズは強力な言葉であり、レイトレーシング、画像処理、パーティクルシミュレーション、メッシュ処理などの非常にパフォーマンスが重要な分野での作業に偏っていますが、弾丸のような非常に軽い処理に使用される小さなオブジェクトを割り当てて解放するのは比較的非常に高価です汎用の可変サイズのメモリアロケーターに対するパーティクル。上記のデータ構造を1日または2日で一般化して必要なものを保存できるはずなので、このようなヒープの割り当て/割り当て解除のコストをすべての小さなものに対して支払うことから完全に排除する価値のある交換になると思います。割り当て/割り当て解除コストの削減に加えて、結果を横断する参照の局所性が向上します(つまり、キャッシュミスやページフォールトが減少します)。
JoshがGCについて言及したことに関して、私はC#のGC実装をJavaほど厳密には研究していませんが、GCアロケーターにはしばしば初期割り当てがありますこれは非常に高速です。これは、メモリを中央から解放できないシーケンシャルアロケータを使用しているためです(ほとんどスタックのように、中央からは削除できません)。次に、メモリをコピーし、以前に割り当てられたメモリ全体をパージすることにより、個々のオブジェクトを個別のスレッドで実際に削除できるようにするための高価なコストを支払います(リンクされた構造のようなものにデータをコピーしながらスタック全体を一度に破棄するなど)、ただし、別のスレッドで実行されるため、必ずしもアプリケーションのスレッドをそれほどストールするわけではありません。ただし、これには、間接レベルの追加という非常に大きな隠れたコストと、最初のGCサイクル後のLORの一般的な損失が伴います。ただし、割り当てを高速化するもう1つの戦略です。呼び出し元のスレッドでそれを安くしてから、別のスレッドで高価な作業を行います。そのため、オブジェクトを参照するのに1レベルではなく2レベルのインダイレクションが必要です。これは、最初に割り当てたときと最初のサイクルの後にメモリでシャッフルされるためです。
C ++で適用するのが少し簡単な、同様の方法でのもう1つの戦略は、メインスレッドでオブジェクトを解放することだけではありません。データ構造の最後に追加と追加と追加を続けるだけで、途中から物を削除することはできません。ただし、削除する必要があるものにマークを付けます。次に、別のスレッドが、削除された要素なしで新しいデータ構造を作成する高価な作業を処理し、次に新しいものを古いものとアトミックに交換します。たとえば、要素の割り当てと解放の両方のコストの要素を削除する要求がすぐに満たされる必要がないと仮定できる場合は、スレッドを分離します。これにより、スレッドに関する限り、解放が安くなるだけでなく、割り当てが安くなります。真ん中からの削除のケースを処理する必要のない、はるかに単純でより暗いデータ構造を使用できるためです。必要なのはコンテナのようなものですpush_back
挿入のためのclear
関数、すべての要素を削除swap
し、削除された要素を除く新しいコンパクトなコンテナと内容を交換するための関数。変異に関する限りはこれで終わりです。