C ++ 17 polymorphic allocators

Very soon, a new stream of the course β€œC ++ Developer. Professional " . On the eve of the start of the course, our expert Alexander Klyuchev prepared an interesting material about polymorphic allocators. We give the floor to Alexander:










In this article, I would like to show simple examples of working with components from the pmr namespace and the basic ideas underlying polymorphic allocators.



The main idea of ​​the polymorphic allocators introduced in c ++ 17 is to improve the standard allocators implemented on the basis of static polymorphism or in other words templates. They are much easier to use than standard allocators, in addition, they allow you to maintain the type of container when using different allocators and, therefore, change allocators at runtime.



If you want std::vectorwith a specific memory allocator, you can use the Allocator template parameter:



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




But there is a problem - this vector is not of the same type as a vector with a different allocator, including one defined by default.

Such a container cannot be passed to a function that requires a vector with a default container, nor can two vectors with different allocator types be assigned to the same variable, for example:



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


A polymorphic allocator contains a pointer to an interface memory_resourceso that it can use dynamic dispatch.



To change the strategy of working with memory, it is enough to replace the instance memory_resource, keeping the type of the allocator. This can be done at runtime as well. Otherwise, polymorphic allocators work according to the same rules as standard ones.



The specific data types used by the new allocator are in the namespace std::pmr. There are also template specializations of standard containers that can work with a polymorphic allocator.



One of the main problems at the moment is the incompatibility of new versions of containers from std::pmrwith analogs from std.



Main components 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()

      The free function returns a pointer to memory_resourcewhich throws an exception std::bad_allocon each allocation attempt.

      This can be useful to ensure that objects do not allocate memory on the heap or for testing purposes.




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

    A thread-safe, general-purpose memory_resource implementation consists of a set of pools with different sizes of memory blocks.

    Each pool is a collection of chunks of memory of the same size.
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    Single threaded version synchronized_pool_resource.
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    Single-threaded, fast, memory_resourcespecial purpose takes memory from a pre-allocated buffer, but does not free it, i.e. it can only grow.


Usage example monotonic_buffer_resourceand 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';
}


Program output:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


In the above example, we used monotonic_buffer_resource, initialized with a buffer allocated on the stack. Using a pointer to this buffer, we can easily display the contents of memory.



The vector takes memory from the pool, which is very fast, since it is on the stack, if it runs out of memory, it requests it using the global operator new. The example demonstrates a vector implementation when trying to insert more than the reserved number of elements. In this case, the monotonic_bufferold memory is not freed, but only grows.



You can, of course, call reserve()on a vector to minimize reallocations, but the purpose of the example is precisely to demonstrate how it changes monotonic_buffer_resourceas the container expands.



Storage pmr::string



What if we want to store strings in pmr::vector?



An important feature is that if objects in a container also use a polymorphic allocator, then they request the parent container's allocator for memory management.



If you want to take advantage of this feature, you must use std::pmr::stringinstead std::string.



Consider an example with a buffer pre-allocated on the stack, which we will pass as memory_resourcefor 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");   
}


Program output:



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


The main points to pay attention to in this example:



  • The size is pmr::stringlarger than std::string. This is due to the fact that a pointer to memory_resource;
  • We reserve the vector for 5 elements, so no reallocations occur when adding 4.
  • The first 2 lines are short enough for the vector memory block, so no additional memory allocation occurs.
  • The third line is longer and required a separate chunk of memory inside our buffer, while the vector stores only a pointer to this block.
  • As you can see from the output, "This is a longer string" is located almost at the very end of the buffer.
  • When we insert another short string, it falls back into the memory block of the vector


For comparison, let's do the same experiment with std::stringinstead ofstd::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###_______________________________________________________________________________________________________________________________#




This time, the items in the container take up less space because there is no need to store a pointer to the memory_resource.

The short strings are still stored inside the vector memory block, but now the long string does not make it into our buffer. This time a long string is allocated using the default allocator and a

pointer to it is placed in the vector memory block . Therefore, we do not see this line in the output.



Once again about vector expansion:



It was mentioned that when the memory in the pool runs out, the allocator requests it using the operator new().



In fact, this is not entirely true - memory is requested from memory_resource, returned using a free function

std::pmr::memory_resource* get_default_resource()

By default, this function returns

std::pmr::new_delete_resource(), which in turn allocates memory using an operator new(), but can be replaced using a function

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



So, let's look at an example when it get_default_resourcereturns a value by default.



It should be borne in mind that the methods do_allocate()and do_deallocate()use the "alignment" argument, so we need the C ++ 17 version new()with alignment support:



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


Now let's get back to looking at the main example:



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


The program tries to put 20 numbers into a vector, but given that the vector is only growing, we need more space than in the reserved buffer with 32 entries.



Therefore, at some point, the allocator will request memory through get_default_resource, which in turn will lead to a call to the global new().



Program output:



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


Judging by the output to the console, the allocated buffer is enough for only 16 elements, and when we insert the number 17, a new allocation of 128 bytes occurs using the operator new().



On the third line, we see a block of memory allocated using an operator new().



The above example with operator override is new()unlikely to be suitable for a product solution.



Fortunately, no one bothers us to make our own implementation of the interface memory_resource.



All we need is



  • inherit from std::pmr::memory_resource
  • Implement methods:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • Pass our implementation to memory_resourcecontainers.


That's all. By the link below you can watch the record of the open day, where we tell in detail about the course program, the learning process and answer questions from potential students:





Read more






All Articles