メモリ 0.6:コンポジションとジョイント アロケータ

私のブログを長く読んでいる方なら、私のメモリ ライブラリを覚えているかもしれません.0.5 のリリースは 2 月でしたが、私はそれを忘れていません!ようやく 0.6 のリリースが完了しました。これは主に 2 つの主要な機能を提供します:コンポジションとジョイント アロケーターです。

foonathan/memory は、さまざまなメモリ アロケータとアダプタ クラスを提供するライブラリです。これらのアロケータは、新しい RawAllocator を使用します。 STL の Allocator よりも単純な概念 割り当ての側面をより適切に制御できます。アダプターと特性により、既存のモデルとの互換性が確保され、STL またはその他のコンテナーでの使用が可能になります。

構成

Andrei の講演は、アロケーターを構成するというアイデアを非常に一般的なものにしました。彼は、多くのアロケーターの「構成要素」を持ち、それらをつなぎ合わせて強力な組み合わせを作成できるライブラリーを提案しました。

BlockAllocator のおかげで コンセプト - 0.5 リリースの投稿または私のミーティング C++ トークで情報を確認してください。すでにいくつかのアロケーターを組み合わせることができます。たとえば、私の virtual_block_allocator を使用できます。 memory_stack を作成する これは仮想メモリ対応です。

しかし、これは彼が説明した種類の構成ではありません.彼のライブラリでは、たとえば fallback_allocator を書くことができました. .これは、2 つのアロケーターを使用するアダプターです。最初に最初のアロケーターを試行し、それが失敗した場合は、2 番目のアロケーターを使用します。

しかし、RawAllocator の割り当てが 失敗し、nullptr を返してはなりません .したがって、失敗したかどうかをチェックすると、代わりにスローされた例外をキャッチすることになります.これは遅いです (ライブラリが例外サポート付きでコンパイルされている場合にのみ機能します).しかし、さらに大きな問題があります:解放.アロケータ メモリが来て、そこで割り当てを解除します。これは、現在の RawAllocator ではサポートされていません 、すべてのアロケーターでサポートできるわけではないため:new_allocator の場合 - ::operator new のラッパー 、メモリがあったかどうかをどのように検出できますか 割り当て解除でそれによって割り当てられましたか?

代わりに、構成可能な RawAllocator という新しい概念を追加しました。 .これは RawAllocator です try_allocate_node/array も提供します と try_deallocate_node/array 関数。try 割り当て関数は nullptr を返します 失敗した場合、例外をスローする/中止する/…の代わりに、try deallocate 関数は、メモリが割り当てから来たかどうかをチェックし、割り当てられた場合にのみ割り当てを解除します。true を返します。 割り当てを解除できる場合、false

構成可能なすべてのアロケータが構成可能になりました。これにより、fallback_operator を実装できます。 :

void* fallback_allocator::allocate_node(std::size_t size, std::size_t alignment)
{
 // first try default
 auto ptr = get_default_allocator()
 .try_allocate_node(size, alignment);
 if (!ptr)
 // default was not successful
 // this is not composable, so guaranteed to be succesful
 ptr = get_fallback_allocator()
 .allocate_node(size, alignment);
 return ptr;
}

void fallback_allocator::deallocate_node(void* ptr,
 std::size_t size, std::size_t alignment) noexcept
{
 // first try default
 auto res = get_default_allocator()
 .try_deallocate_node(ptr,
 size, alignment);
 if (!res)
 // could not be allocated by default
 get_fallback_allocator()
 .deallocate_node(ptr, size, alignment);
}

fallback_allocator に加えて 、 segregator も実装しました .

これは、1 つ以上の Segregatable を取るアロケータ アダプタです。 と RawAllocator .A Segregatable アロケータを所有し、このアロケータを使用するかどうかを割り当てごとに決定できる単純なクラスです。最も基本的な Segregatable threshold_segregatable です .指定された最大サイズまでの割り当てを処理します。

segregator Segregatable ごとに聞いてみましょう 次に、その割り当てが必要な場合は、最初に割り当てたものを使用します。ない場合 Segregatable 必要に応じて、RawAllocator を使用します 割り当て:

auto seg = memory::make_segregator(memory::threshold(16u, std::move(small_alloc)),
 memory::threshold(128u, std::move(medium_alloc)),
 std::move(big_alloc));
seg.allocate_node(8, 4); // uses small_alloc
seg.allocate_node(32, 8); // uses medium alloc
seg.allocate_node(4_KiB, 8); // uses big_alloc

null_allocator も追加しました :何も割り当てず、すべての呼び出しで例外が発生するアロケーター。segregator に役立ちます。 :最終的な RawAllocator として渡します 少なくともいくつかの Segregatable を確保するために

ジョイント メモリの割り当て

また、この素晴らしい投稿に触発されて、共同メモリ割り当ての機能も追加しました。次のタイプを検討してください:

struct my_type
{
 std::string str;
 std::vector<int> vec;

 my_type(const char* name)
 : str(name), vec({1, 2, 3, 4, 5})
 {}
};

動的に割り当てるとどうなるか考えてみましょう:std::string のコンストラクター と std::vector (あなたの知識人にとっては「可能性があります」) も動的メモリを割り当てます。

ここで、共同割り当てが役立ちます。オブジェクト自体に必要なメモリ ブロックよりも大きなメモリ ブロックを割り当て、追加のメモリ (「共同メモリ」) をメンバーの動的割り当てに使用するという考え方です。

メモリに実装した機能を使用すると、これは非常に簡単です。

struct my_type : memory::joint_type<my_type>
{
 memory::string<memory::joint_allocator> str;
 memory::joint_array<int> vec;

 my_type(memory::joint tag, const char* name)
 : memory::joint_type<my_type>(tag),
 str(name, *this),
 vec({1, 2, 3, 4, 5}, *this)
 {}
};

my_type を変更する必要があります 最初に行うことは、memory::joint_type から継承することです。 .このベースは、ジョイント メモリを管理するための 2 つのポインターを挿入します。

次に、動的割り当てを持つ各メンバーは joint_allocator を使用する必要があります ジョイント メモリを使用するため。joint_allocator RawAllocator です これは、動的メモリ割り当てのために特定のオブジェクトの共同メモリを使用します。この場合、std::string で使用します。 .

memory::joint_allocator オーバーヘッドが少しあります - 正確には追加のポインター、memory::joint_array<T> もあります .これは動的な固定サイズの配列、つまり std::vector<T> です ジョイント メモリを使用するように設計されており、オーバーヘッドはありません。

ジョイント タイプのすべてのコンストラクターは、memory::joint のオブジェクトも取得する必要があります。 最初のパラメーターとして。このオブジェクトには 2 つのジョブがあります。まず、friend によってのみ作成できます。 s、したがって、ジョイント メモリのないジョイント タイプの偶発的な作成が禁止されます。次に、ジョイント メモリに関するメタデータが含まれており、joint_type に渡す必要があります。 .

カスタム アロケーターのため、アロケーターをオブジェクトに渡す必要があります。これは単純です *this 、共同記憶を持つオブジェクト。

ジョイント タイプを作成するには、allocate_joint を使用します 関数:

auto ptr = memory::allocate_joint<my_type>
 (memory::default_allocator{},
 memory::joint_size(…),
 "joint!");
 
std::cout << ptr->str << '\n';
for (auto& el : *ptr)
 std::cout << el << ' ';
std::cout << '\n';

この関数は、 - single! に使用されるアロケータを取ります。 - 割り当て、ジョイント メモリのサイズ、および型コンストラクタに渡される追加の引数。サイズの型は memory::joint_size です。 std::size_t から明示的に変換可能 .ジョイント メモリの唯一の欠点は、事前にサイズを手動で計算することです。その際、アラインメント バッファーも考慮に入れる必要があります。サイズが十分でない場合は、例外がスローされます。

allocate_joint の戻り型 memory::joint_ptr<T, RawAllocator> です .これは std::unique_ptr<T> と同様に動作します 、ただし、ジョイント メモリ ブロック全体を所有し、範囲外になると割り当てを解除します。

詳細については、例を確認してください。

アロケータの伝播について

私の最初の本当のブログ投稿で、STL Allocator がどのように機能するかについて話しました。 モデルにはこれらの propagate_on_XXX があります typedefs.これらは、コンテナーがコピー/移動割り当て/スワップされたときに、アロケーターがコピー/移動割り当て/スワップされるかどうかを制御します。select_on_container_copy_construction() メンバー関数は、コンテナー コピーの構築で何が起こるかを制御します。移動の構築はカスタマイズできません。

その投稿で、伝播なしのデフォルトは、パフォーマンスの悲観化、未定義で非直感的な動作につながる可能性があるため、悪いと述べました.コンテナ割り当てがアロケータも割り当てるように、常にデフォルトを変更する必要があることを提案しました.

ブログ投稿の後、アロケーター モデルのその部分を設計した Alisdair Meredith から電子メールを受け取りました.彼は、主にアロケーターがメンバーと共有されるコンテナーのため、選択の背後にある理由を説明しました.私はこれについて詳しく書きましたなぜこれが必要なのかよくわかりませんでしたが、自分で状況に遭遇しなかったので、それ以上コメントしませんでした.

しかし、共同割り当てで、私はやった 2 つのジョイント オブジェクトがあり、それらを割り当てるとどうなるかを考えてみましょう:

auto a = memory::allocate_joint<my_type>(…);
auto b = memory::allocate_joint<my_type>(…);

*a = *b;

これにより、すべてのメンバーが割り当てられるため、str も割り当てられます。 container.str joint_allocator を使用 std_allocator の中 RawAllocator を使用できるアダプター std_allocator 内のデフォルトの伝搬選択 は常にコンテナーを伝播します。これは、元の投稿で作成したガイドラインでした。

したがって、コンテナの代入演算子は a->str からアロケータを割り当てます b->str が使用するアロケータへ .str a からのオブジェクト b からの共同メモリを使用してアロケータを使用します !b 最初は十分なメモリがないかもしれませんが、b を想像してみてください。 aより前に破壊される .これは b も破壊します のメモリなので、a 破壊されたメモリを使用するようになりました。

これは悪いことなので、ここでは伝播は正しい選択ではありません。コンテナが割り当てられたときにアロケータが割り当てられることは望ましくありません - スワップと同様です。2 つのコンテナを等しくないアロケータで交換することは未定義の動作であるため、これはコンテナ間のスワップを禁止します。異なるジョイント メモリの場合、ジョイント オブジェクトのメンバー間の交換のみが許可されます。

同じ問題がコピー構築にも存在します。my_type のコピー コンストラクタを記述した場合 そのように:

my_type(memory::joint tag, const joint_type& other)
: memory::joint_type<my_type>(tag),
 str(other.str),
 vec(other.vec)
{}

str other.str からアロケータをコピーします 、したがって、other からのジョイント メモリを使用します。 *this の代わりに .アロケータを取るコピー コンストラクタ バージョンを使用する必要があります:

str(other.str, *this) // copy construct str using *this as allocator

幸いなことに、コピー コンストラクション コール select_on_container_copy_construction() 、だから static_assert() を入れて その中で、このコードのコンパイルを止めることができます。残念ながら、select_on_container_move_construction() はありません。 、だから気をつけなきゃ。

std_allocator による伝播動作を制御するには ,デフォルトの動作を propagation_traits に入れました .独自の RawAllocator に特化することができます std_allocator の伝播動作を制御します .

マイナーな機能

これら 2 つの主要な機能に加えて、いくつかの小さな機能を実装しました。

ブロック サイズ リテラル

アリーナ アロケータ (memory::memory_pool など) を使用している場合 、 memory::memory_stack ,…),あなたはよく次のように作成します:

memory::memory_pool<> pool(16, 4096);

4096 はアリーナの初期サイズなので、4KiB です。便宜上、それらにユーザー定義のリテラルを追加したので、次のように記述できます。

using namespace memory::literals;
memory::memory_pool<> pool(16, 4_KiB);

ヘッダー memory_arena.hpp 1024 の倍数になる KiB、MiB、GiB のユーザー定義リテラルを提供するようになりました KB、MB、GB は 1000 の倍数になります .単純に std::size_t を返します .

temporary_allocator 改善

temporary_allocator は、一時的な割り当てのための機能です。高速な割り当てを可能にするために、グローバルなスレッドローカル スタックを使用します。

今回の更新で、スタックは temporary_stack として公開されました 作成を制御できるようになりました。マクロ FOONATHAN_MEMORY_TEMPORARY_STACK_MODE 2 つの 0 を設定できます 、 1 または 2 .

0 スタックが自動的に作成されないことを意味します。temporary_stack をクレートする必要があります。 トップレベル関数で自分自身に反対し、それを伝えます。

1get_temporary_stack() を呼び出すことで、スレッドごとに 1 つのスタックを利用できます。 ですが、自動的に破棄されることはありません。そのためには、temporary_stack_initializer を使用する必要があります クラス、トップレベル関数のオブジェクトで作成、デストラクタはスタックを破棄します。

そして 2 で スタックは自動的に破棄されますが、わずかなランタイム オーバーヘッドが発生します。引き続き temporary_stack_initializer を使用できます ただし、もう必要ありません。

スタック アロケータの追加

memory_stack_raii_unwind を追加しました iteration_allocator と同様に、あなたが思っていることを正確に実行します .

iteration_allocator ループ内で多くの割り当てを行う場合に設計されており、各割り当ては N の間存続する必要があります これはダブル フレーム アロケータの一般化です。N で構成されます。 メモリは内部的にスタックされ、反復ごとに切り替えられます。スタックに戻ると、スタックがクリアされ、すべてのメモリが解放されます:

// creates it with 2 stacks,
// each one using 2KiB memory
memory::iteration_allocator<2> alloc(4_KiB);

while (…)
{
 auto mem = alloc.allocate(…);
 // mem now lives for two iterations
 
 …

 // switch stacks
 alloc.next_iteration(); 
}

結論

このアップデートには、OS X のサポートと多くのバグ修正も含まれています。

ドキュメンテーションは現在も Doxygen を使用していますが、標準としてはほぼ使用できる状態にあるため、すぐに移行し、ドキュメンテーションも改善します。

それまでの間、私の Meeting C++ の講演のスライドをチェックして、ライブラリを試してみることもできます。次の更新は、おそらくスレッドごとのアロケータに取り組み、おそらく最後の 0.x になるでしょう。 バージョン。

いつものように:フィードバックや機能のリクエストなどをお待ちしておりますので、お気軽にお問い合わせください!