C ++ 17多形アロケーター

間もなく、コース「C ++ Developer」の新しいストリームプロフェッショナル」コース開始の前夜、専門家のアレクサンダー・クリチェフが多形アロケーターに関する興味深い資料を作成しました。私たちはアレクサンダーに床を与えます:










この記事では、pmr名前空間のコンポーネントを操作する簡単な例と、ポリモーフィックアロケーターの基礎となる基本的な考え方を示します。



c ++ 17で導入されたポリモーフィックアロケーターの主なアイデアは、静的ポリモーフィズム、つまりテンプレートに基づいて実装された標準アロケーターを改善することです。これらは、標準のアロケーターよりもはるかに使いやすく、さらに、さまざまなアロケーターを使用するときにコンテナーのタイプを維持できるため、実行時にアロケーターを変更できます。



あなたがしたい場合はstd::vector、特定のメモリアロケータで、あなたはアロケータテンプレートパラメータを使用することができます。



auto my_vector = std::vector<int, my_allocator>();




ただし、問題があります。このベクトルは、デフォルトで定義されているものを含め、アロケーターが異なるベクトルと同じタイプではありません。

このようなコンテナは、デフォルトのコンテナを持つベクトルを必要とする関数に渡すことはできません。また、異なるアロケータタイプを持つ2つのベクトルを同じ変数に割り当てることもできません。たとえば、次のようになります。



auto my_vector = std::vector<int, my_allocator>();
auto my_vector2 = std::vector<int, other_allocator>();
auto vec = my_vector; // ok
vec = my_vector2; // error


ポリモーフィックアロケータには、memory_resource動的ディスパッチを使用できるように、インターフェイスへのポインタが含まれています



メモリを操作する戦略を変更するにmemory_resourceは、アロケータのタイプを維持したまま、インスタンスを置き換えるだけで十分です。これは実行時にも実行できます。それ以外の場合、ポリモーフィックアロケーターは標準のアロケーターと同じルールに従って機能します。



新しいアロケータによって使用される特定のデータタイプは、名前空間にありますstd::pmr多形アロケータで動作できる標準コンテナのテンプレート特殊化もあります。



現時点での主な問題の1つは、からの新しいバージョンのコンテナとからのstd::pmrアナログとの非互換性ですstd



メインコンポーネント std::pmr:



  • std::pmr::memory_resource — , .
  • :

    • virtual void* do_allocate(std::size_t bytes, std::size_t alignment),
    • virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment)
    • virtual bool do_is_equal(const std::pmr::memory_resource& other) const noexcept.
  • std::pmr::polymorphic_allocator — , memory_resource .
  • new_delete_resource() null_memory_resource() «»
  • :

    • synchronized_pool_resource
    • unsynchronized_pool_resource
    • monotonic_buffer_resource
  • , std::pmr::vector, std::pmr::string, std::pmr::map . , .
  • memory_resource:

    • memory_resource* new_delete_resource() , memory_resource, new delete .
    • memory_resource* null_memory_resource()

      free関数は、割り当ての試行ごとにmemory_resource例外をスローするポインタを返しますstd::bad_alloc

      これは、オブジェクトがヒープにメモリを割り当てないようにするため、またはテスト目的で役立ちます。




  • class synchronized_pool_resource : public std::pmr::memory_resource

    スレッドセーフな汎用memory_resource実装は、さまざまなサイズのメモリブロックを持つプールのセットで構成されます。

    各プールは、同じサイズのメモリのチャンクのコレクションです。
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    シングルスレッドバージョンsynchronized_pool_resource
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    シングルスレッドの高速 memory_resourceな特殊用途は、事前に割り当てられたバッファからメモリを取得しますが、それを解放しません。つまり、拡張することしかできません。


使用例monotonic_buffer_resourcepmr::vector



#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>        	// pmr::vector
#include <string>        	// pmr::string
 
int main() {
	char buffer[64] = {}; // a small buffer on the stack
	std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
	std::cout << buffer << '\n';
 
	std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
 
	std::pmr::vector<char> vec{ &pool };
	for (char ch = 'a'; ch <= 'z'; ++ch)
    	vec.push_back(ch);
 
	std::cout << buffer << '\n';
}


プログラム出力:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


上記の例ではmonotonic_buffer_resource、スタックに割り当てられたバッファで初期化されたを使用しました。このバッファへのポインタを使用して、メモリの内容を簡単に表示できます。



ベクターはプールからメモリを取得しますが、これはスタック上にあるため非常に高速です。メモリが不足すると、グローバル演算子を使用して要求しますnewこの例は、予約された数を超える要素を挿入しようとしたときのベクトルの実装を示しています。この場合、monotonic_buffer古いメモリ解放されず、大きくなるだけです。



もちろん、reserve()再割り当てを最小限に抑えるためにベクトルを呼び出すこともできますが、この例の目的はmonotonic_buffer_resource、コンテナーが拡張するときにベクトルがどのように変化するかを正確に示すことです。



ストレージ pmr::string



文字列をに格納したい場合はどうなりpmr::vectorますか?



重要な機能は、コンテナ内のオブジェクトもポリモーフィックアロケータを使用している場合、メモリ管理のために親コンテナのアロケータを要求することです。



この機能を利用したい場合は、std::pmr::string代わりにを使用する必要がありますstd::string



我々のように通過するスタック上に予め割り当てられたバッファを用いて一例を検討memory_resourceするためのstd::pmr::vector std::pmr::string



#include <iostream>
#include <memory_resource>   // pmr core types
#include <vector>        	// pmr::vector
#include <string>        	// pmr::string
 
int main() {
	std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
	std::cout << "sizeof(std::pmr::string): " << sizeof(std::pmr::string) << '\n';
 
	char buffer[256] = {}; // a small buffer on the stack
	std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
 
	const auto BufferPrinter = [](std::string_view buf, std::string_view title) {
    	std::cout << title << ":\n";
    	for (auto& ch : buf) {
        	std::cout << (ch >= ' ' ? ch : '#');
    	}
    	std::cout << '\n';
	};
 
	BufferPrinter(buffer, "zeroed buffer");
 
	std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
	std::pmr::vector<std::pmr::string> vec{ &pool };
	vec.reserve(5);
 
	vec.push_back("Hello World");
	vec.push_back("One Two Three");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after two short strings");
 
	vec.emplace_back("This is a longer string");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after longer string strings");
 
	vec.push_back("Four Five Six");
	BufferPrinter(std::string_view(buffer, std::size(buffer)), "after the last string");   
}


プログラム出力:



sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________#
after longer string strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________________________________________________________________________________________This is a longer string#_______________________________#
after the last string:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________#m### ###n### ##########Four Five Six###________________________________________This is a longer string#_______________________________#


この例で注意すべき主なポイントは次のとおりです。



  • サイズがpmr::stringより大きいstd::stringこれは、memory_resource;へのポインタが
  • ベクトルを5つの要素用に予約しているため、4を追加しても再割り当ては発生しません。
  • 最初の2行は、ベクトルメモリブロックに対して十分に短いため、追加のメモリ割り当ては発生しません。
  • 3行目は長く、バッファ内に個別のメモリチャンクが必要であり、このブロックへのポインタのみがベクターに格納されます。
  • 出力からわかるように、「これは長い文字列です」は、バッファのほぼ最後にあります。
  • 別の短い文字列を挿入すると、ベクトルのメモリブロックにフォールバックします


比較のために、std::string代わりに同じ実験をしてみましょうstd::pmr::string



sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
###w# ##########Hello World########w# ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________________________#
new 24
after longer string strings:
###w# ##########Hello World########w# ##########One Two Three###0#######################_______________________________________________________________________________________________________________________________________________________________________#
after the last string:
###w# ##########Hello World########w# ##########One Two Three###0#######################________@##w# ##########Four Five Six###_______________________________________________________________________________________________________________________________#




今回は、memory_resourceへのポインターを格納する必要がないため、コンテナー内のアイテムが占めるスペースが少なくなります。

短い文字列は引き続きベクトルメモリブロック内に格納されますが、長い文字列はバッファに入れられません。今回は、デフォルトのアロケータを使用して長い文字列が割り当てられ、その文字列

ポインタがベクトルメモリブロックに配置されますしたがって、この行は出力に表示されません。



もう一度ベクトル展開について:



プール内のメモリがなくなると、アロケータは演算子を使用してメモリを要求すると述べられていますnew()



実際、これは完全に真実ではありません-メモリはから要求されmemory_resource、free関数を使用して返され

std::pmr::memory_resource* get_default_resource()

ますデフォルトでは、この関数はを返し

std::pmr::new_delete_resource()、演算子を使用してメモリを割り当てますnew()が、関数を使用して置き換えることができます。

std::pmr::memory_resource* set_default_resource(std::pmr::memory_resource* r)



では、get_default_resourceによって値を返す場合の例を見てみましょう。デフォルト。



メソッドdo_allocate()do_deallocate()「alignment」引数使用することに留意する必要があるためnew()、アライメントをサポートするC ++ 17バージョンが必要です



void* lastAllocatedPtr = nullptr;
size_t lastSize = 0;
 
void* operator new(std::size_t size, std::align_val_t align) {
#if defined(_WIN32) || defined(__CYGWIN__)
	auto ptr = _aligned_malloc(size, static_cast<std::size_t>(align));
#else
	auto ptr = aligned_alloc(static_cast<std::size_t>(align), size);
#endif
 
	if (!ptr)
    	throw std::bad_alloc{};
 
	std::cout << "new: " << size << ", align: "
          	<< static_cast<std::size_t>(align)
  	        << ", ptr: " << ptr << '\n';
 
	lastAllocatedPtr = ptr;
	lastSize = size;
 
	return ptr;
}


ここで、主な例に戻りましょう。



constexpr auto buf_size = 32;
uint16_t buffer[buf_size] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, 0);
 
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)*sizeof(uint16_t)};
 
std::pmr::vector<uint16_t> vec{ &pool };
 
for (int i = 1; i <= 20; ++i)
	vec.push_back(i);
 
for (int i = 0; i < buf_size; ++i)
	std::cout <<  buffer[i] << " ";
 
std::cout << std::endl;
 
auto* bufTemp = (uint16_t *)lastAllocatedPtr;
 
for (unsigned i = 0; i < lastSize; ++i)
	std::cout << bufTemp[i] << " ";


プログラムは20個の数字をベクトルに入れようとしますが、ベクトルが大きくなるだけなので、32個のエントリがある予約済みバッファーよりも多くのスペースが必要です。



したがって、ある時点で、アロケータはを介してメモリを要求しますget_default_resource。これにより、グローバルへの呼び出しが発生しnew()ます。



プログラム出力:



new: 128, align: 16, ptr: 0xc73b20
1 1 2 1 2 3 4 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 132 0 0 0 0 0 0 0 144 0 0 0 65 0 0 0 16080 199 0 0 16176 199 0 0 16176 199 0 0 15344 199 0 0 15472 199 0 0 15472 199 0 0 0 0 0 0 145 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0


コンソールへの出力から判断すると、割り当てられたバッファは16要素のみで十分であり、数値17を挿入すると、演算子を使用して128バイトの新しい割り当てが発生しますnew()



3行目には、演算子を使用して割り当てられたメモリのブロックが表示されますnew()



演算子オーバーライドを使用した上記の例new()は、製品ソリューションに適している可能性低いです。



幸いなことに、インターフェイスの独自の実装を作成することを誰も気にしませんmemory_resource



必要なのは



  • から継承 std::pmr::memory_resource
  • メソッドの実装:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • 実装をmemory_resourceコンテナに渡します


それで全部です。以下のリンクから、オープンデーの記録を見ることができます。ここでは、コースプログラム、学習プロセスについて詳しく話し、潜在的な学生からの質問に答えます。





続きを読む






All Articles