Pengalokasi polimorfik C ++ 17

Segera, aliran baru dari kursus “Pengembang C ++. Profesional " . Menjelang permulaan kursus, pakar kami Alexander Klyuchev menyiapkan materi menarik tentang pengalokasi polimorfik. Kami memberikan lantai kepada Alexander:










Pada artikel ini, saya ingin menunjukkan contoh sederhana bekerja dengan komponen dari namespace pmr dan ide dasar yang mendasari pengalokasi polimorfik.



Ide utama dari pengalokasi polimorfik yang diperkenalkan di c ++ 17 adalah untuk meningkatkan pengalokasi standar yang diimplementasikan berdasarkan polimorfisme statis atau dengan kata lain templat. Mereka jauh lebih mudah digunakan daripada pengalokasi standar, selain itu, mereka memungkinkan Anda mempertahankan jenis penampung saat menggunakan pengalokasi yang berbeda dan, oleh karena itu, mengubah pengalokasi pada waktu proses.



Jika Anda ingin std::vectordengan pengalokasi memori tertentu, Anda dapat menggunakan parameter template Allocator:



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




Tetapi ada masalah - vektor ini tidak berjenis sama dengan vektor dengan pengalokasi berbeda, termasuk yang ditentukan secara default.

Penampung seperti itu tidak dapat diteruskan ke fungsi yang membutuhkan vektor dengan penampung default, dan dua vektor dengan jenis pengalokasi berbeda juga tidak dapat diberikan ke variabel yang sama, misalnya:



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


Pengalokasi polimorfik berisi penunjuk ke antarmuka memory_resourcesehingga dapat menggunakan pengiriman dinamis.



Untuk mengubah strategi bekerja dengan memori, itu cukup untuk mengganti instance memory_resource, menjaga jenis pengalokasi. Ini juga dapat dilakukan saat runtime. Jika tidak, pengalokasi polimorfik bekerja sesuai dengan aturan yang sama seperti aturan standar.



Tipe data spesifik yang digunakan oleh pengalokasi baru ada di namespace std::pmr. Ada juga spesialisasi templat wadah standar yang dapat bekerja dengan pengalokasi polimorfik.



Salah satu masalah utama saat ini adalah ketidakcocokan versi baru penampung std::pmrdengan analog dari std.



Komponen utama 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()

      Fungsi bebas mengembalikan sebuah pointer memory_resourceyang akan memunculkan pengecualian std::bad_allocpada setiap upaya alokasi.

      Ini bisa berguna untuk memastikan bahwa objek tidak mengalokasikan memori di heap atau untuk tujuan pengujian.




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

    Implementasi memory_resource tujuan umum yang aman untuk thread terdiri dari kumpulan kumpulan dengan ukuran blok memori yang berbeda.

    Setiap kumpulan adalah kumpulan potongan memori dengan ukuran yang sama.
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    Versi berulir tunggal synchronized_pool_resource.
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    Utas tunggal, cepat, memory_resourcetujuan khusus mengambil memori dari buffer yang dialokasikan sebelumnya, tetapi tidak membebaskannya, artinya, hanya dapat berkembang.


Contoh penggunaan monotonic_buffer_resourcedan pmr::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';
}


Keluaran program:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


Dalam contoh di atas, kami menggunakan monotonic_buffer_resourceinisialisasi dengan buffer yang dialokasikan di stack. Dengan menggunakan pointer ke buffer ini, kita dapat dengan mudah menampilkan isi memori.



Vektor mengambil memori dari pool, yang sangat cepat, karena berada di stack, jika kehabisan memori, ia memintanya menggunakan operator global new. Contoh ini menunjukkan implementasi vektor ketika mencoba memasukkan lebih dari jumlah elemen yang dicadangkan. Dalam hal ini, monotonic_buffermemori lama tidak dibebaskan, tetapi hanya bertambah.



Anda dapat, tentu saja, memanggil reserve()vektor untuk meminimalkan realokasi, tetapi tujuan dari contoh ini justru untuk mendemonstrasikan bagaimana hal itu berubah monotonic_buffer_resourcesaat container mengembang.



Penyimpanan pmr::string



Bagaimana jika kita ingin menyimpan string pmr::vector?



Fitur penting adalah jika objek dalam wadah juga menggunakan pengalokasi polimorfik, maka mereka meminta pengalokasi penampung induk untuk mengelola memori.



Jika Anda ingin mengambil keuntungan dari fitur ini, Anda harus menggunakan std::pmr::stringgantinya std::string.



Pertimbangkan contoh dengan buffer pra-dialokasikan pada stack, yang kita akan lulus sebagai memory_resourceuntuk 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");   
}


Keluaran program:



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#_______________________________#


Poin utama yang perlu diperhatikan dalam contoh ini:



  • Ukurannya pmr::stringlebih besar dari std::string. Hal ini disebabkan oleh fakta bahwa pointer ke memory_resource;
  • Kami mencadangkan vektor untuk 5 elemen, jadi tidak ada realokasi yang terjadi saat menambahkan 4.
  • 2 baris pertama cukup pendek untuk blok memori vektor, jadi tidak ada alokasi memori tambahan yang terjadi.
  • Baris ketiga lebih panjang dan membutuhkan potongan memori terpisah di dalam buffer kami, dan hanya penunjuk ke blok ini yang disimpan dalam vektor.
  • Seperti yang Anda lihat dari output, "Ini adalah string yang lebih panjang" terletak hampir di ujung buffer.
  • Saat kita memasukkan string pendek lainnya, string itu jatuh kembali ke blok memori vektor


Sebagai perbandingan, mari lakukan eksperimen yang sama dengan std::stringalih - alihstd::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###_______________________________________________________________________________________________________________________________#




Kali ini, item dalam container mengambil lebih sedikit ruang karena tidak perlu menyimpan pointer ke memory_resource.

String pendek masih disimpan di dalam blok memori vektor, tetapi sekarang string panjang tidak masuk ke buffer kami. Kali ini string panjang dialokasikan menggunakan pengalokasi default dan

penunjuk ke sana ditempatkan di blok memori vektor . Oleh karena itu, kami tidak melihat baris ini di keluaran.



Sekali lagi tentang ekspansi vektor:



Disebutkan bahwa ketika memori di pool habis, pengalokasi memintanya menggunakan operator new().



Faktanya, ini tidak sepenuhnya benar - memori diminta dari memory_resource, dikembalikan menggunakan fungsi bebas

std::pmr::memory_resource* get_default_resource()

Secara default, fungsi ini kembali

std::pmr::new_delete_resource(), yang pada gilirannya mengalokasikan memori menggunakan operator new(), tetapi dapat diganti menggunakan fungsi.

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



Jadi, mari kita lihat contoh ketika ia get_default_resourcemengembalikan nilai dengan default.



Perlu diingat bahwa metode do_allocate()dan do_deallocate()menggunakan argumen "penyelarasan", jadi kami memerlukan versi C ++ 17 new()dengan dukungan penyelarasan:



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;
}


Sekarang mari kita kembali melihat contoh utama:



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] << " ";


Program mencoba untuk memasukkan 20 angka ke dalam vektor, tetapi mengingat bahwa vektor hanya berkembang, kita membutuhkan lebih banyak ruang daripada di buffer yang dipesan dengan 32 entri.



Oleh karena itu, pada titik tertentu, pengalokasi akan meminta melalui memori get_default_resource, yang pada gilirannya akan menyebabkan panggilan ke global new().



Keluaran program:



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


Dilihat dari output ke konsol, buffer yang dialokasikan cukup untuk 16 elemen saja, dan ketika kita memasukkan nomor 17, alokasi baru 128 byte terjadi menggunakan operator new().



Pada baris ketiga, kita melihat blok memori yang dialokasikan menggunakan operator new().



Contoh di atas dengan penggantian operator new()tidak mungkin cocok untuk solusi produk.



Untungnya, tidak ada yang mengganggu kami untuk membuat implementasi antarmuka kami sendiri memory_resource.



Semua yang kita butuhkan adalah



  • mewarisi dari std::pmr::memory_resource
  • Menerapkan metode:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • Teruskan implementasi kami ke memory_resourcecontainer.


Itu saja. Melalui tautan di bawah ini Anda dapat menyaksikan rekaman hari open house, di mana kami menceritakan secara detail tentang program kursus, proses pembelajaran dan menjawab pertanyaan dari calon siswa:





Baca lebih banyak






All Articles