Asignadores polimórficos de C ++ 17

Muy pronto, una nueva corriente del curso “C ++ Developer. Profesional " . La víspera del inicio del curso, nuestro experto Alexander Klyuchev preparó un material interesante sobre asignadores polimórficos. Le damos la palabra a Alejandro:










En este artículo, me gustaría mostrar ejemplos simples de cómo trabajar con componentes del espacio de nombres pmr y las ideas básicas subyacentes a los asignadores polimórficos.



La idea principal de los asignadores polimórficos introducidos en c ++ 17 es mejorar los asignadores estándar implementados sobre la base del polimorfismo estático o, en otras palabras, plantillas. Son mucho más fáciles de usar que los asignadores estándar, además, te permiten mantener el tipo de contenedor cuando usas diferentes asignadores y, por lo tanto, cambiar asignadores en tiempo de ejecución.



Si lo desea std::vectorcon un asignador de memoria específico, puede usar el parámetro de plantilla del asignador:



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




Pero hay un problema: este vector no es del mismo tipo que un vector con un asignador diferente, incluido uno definido por defecto.

Dicho contenedor no se puede pasar a una función que requiere un vector con un contenedor predeterminado, ni se pueden asignar dos vectores con diferentes tipos de asignador a la misma variable, por ejemplo:



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


Un asignador polimórfico contiene un puntero a una interfaz memory_resourcepara que pueda utilizar el envío dinámico.



Para cambiar la estrategia de trabajar con memoria, basta con reemplazar la instancia memory_resource, manteniendo el tipo de asignador. Esto también se puede hacer en tiempo de ejecución. De lo contrario, los asignadores polimórficos funcionan de acuerdo con las mismas reglas que los estándar.



Los tipos de datos específicos utilizados por el nuevo asignador están en el espacio de nombres std::pmr. También hay especializaciones de plantilla de contenedores estándar que pueden funcionar con un asignador polimórfico.



Uno de los principales problemas en este momento es la incompatibilidad de las nuevas versiones de contenedores de std::pmrcon los análogos de std.



Componentes principales 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()

      La función libre devuelve un puntero al memory_resourceque arroja una excepción std::bad_allocen cada intento de asignación.

      Esto puede resultar útil para garantizar que los objetos no asignen memoria en el montón o con fines de prueba.




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

    Una implementación memory_resource de propósito general y segura para subprocesos consiste en un conjunto de grupos con diferentes tamaños de bloques de memoria.

    Cada grupo es una colección de fragmentos de memoria del mismo tamaño.
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    Versión de un solo hilo synchronized_pool_resource.
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    Un solo subproceso, rápido y memory_resourcede propósito especial toma memoria de un búfer preasignado, pero no la libera, es decir, solo puede crecer.


Ejemplo de uso monotonic_buffer_resourcey 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';
}


Salida del programa:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


En el ejemplo anterior, usamos monotonic_buffer_resource, inicializado con un búfer asignado en la pila. Usando un puntero a este búfer, podemos mostrar fácilmente el contenido de la memoria.



El vector toma memoria del pool, lo cual es muy rápido, ya que está en la pila, si se queda sin memoria lo solicita usando el operador global new. El ejemplo demuestra una implementación de vector cuando se intenta insertar más del número reservado de elementos. En este caso, la monotonic_buffermemoria antigua no se libera, solo crece.



Por supuesto, puede llamar reserve()a un vector para minimizar las reasignaciones, pero el propósito del ejemplo es precisamente demostrar cómo cambia a monotonic_buffer_resourcemedida que se expande el contenedor.



Almacenamiento pmr::string



¿Qué pasa si queremos almacenar cadenas pmr::vector?



Una característica importante es que si los objetos en un contenedor también usan un asignador polimórfico, entonces solicitan el asignador del contenedor principal para la gestión de la memoria.



Si desea aprovechar esta función, debe usarla std::pmr::stringen su lugar std::string.



Veamos un ejemplo con un tampón preasignados en la pila, lo que vamos a pasar como memory_resourcea 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");   
}


Salida del programa:



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


Los principales puntos a los que prestar atención en este ejemplo:



  • El tamaño es pmr::stringmayor que std::string. Esto se debe al hecho de que un puntero a memory_resource;
  • Reservamos el vector para 5 elementos, por lo que no se producen reasignaciones al agregar 4.
  • Las primeras 2 líneas son lo suficientemente cortas para el bloque de memoria vectorial, por lo que no se produce una asignación de memoria adicional.
  • La tercera línea es más larga y requiere una porción de memoria separada dentro de nuestro búfer, y solo el puntero a este bloque se almacena en el vector.
  • Como puede ver en la salida, "Esta es una cadena más larga" se encuentra casi al final del búfer.
  • Cuando insertamos otra cadena corta, vuelve al bloque de memoria del vector


A modo de comparación, hagamos el mismo experimento con en std::stringlugar destd::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###_______________________________________________________________________________________________________________________________#




Esta vez, los elementos del contenedor ocupan menos espacio porque no hay necesidad de almacenar un puntero al memory_resource.

Las cadenas cortas todavía se almacenan dentro del bloque de memoria vectorial, pero ahora la cadena larga no entró en nuestro búfer. Esta vez, se asigna una cadena larga utilizando el asignador predeterminado y

se coloca un puntero en el bloque de memoria vectorial . Por lo tanto, no vemos esta línea en la salida.



Una vez más sobre la expansión vectorial:



Se mencionó que cuando se agota la memoria en el grupo, el asignador lo solicita utilizando el operador new().



De hecho, esto no es del todo cierto: la memoria se solicita memory_resourcey se devuelve usando una función

std::pmr::memory_resource* get_default_resource()

libre.Por defecto, esta función regresa

std::pmr::new_delete_resource(), que a su vez asigna memoria usando un operador new(), pero puede ser reemplazada usando una función.

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



Entonces, veamos un ejemplo cuando get_default_resourcedevuelve un valor por defecto.



Debe tenerse en cuenta que los métodos do_allocate()y do_deallocate()usan el argumento de "alineación", por lo que necesitamos la versión C ++ 17 new()con soporte de alineación:



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


Ahora volvamos a mirar el ejemplo principal:



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


El programa intenta poner 20 números en un vector, pero dado que el vector solo está creciendo, necesitamos más espacio que en el búfer reservado con 32 entradas.



Por lo tanto, en algún momento, el asignador solicitará memoria intermedia get_default_resource, lo que a su vez conducirá a una llamada al global new().



Salida del programa:



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


A juzgar por la salida a la consola, el búfer asignado es suficiente para solo 16 elementos, y cuando insertamos el número 17, se produce una nueva asignación de 128 bytes utilizando el operador new().



En la tercera línea, vemos un bloque de memoria asignado mediante un operador new().



Es new()poco probable que el ejemplo anterior con anulación del operador sea ​​adecuado para una solución de producto.



Afortunadamente, nadie nos molesta en hacer nuestra propia implementación de la interfaz memory_resource.



Todo lo que necesitamos es



  • heredar de std::pmr::memory_resource
  • Implementar métodos:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • Pasar nuestra implementación a memory_resourcecontenedores.


Eso es todo. Mediante el enlace de abajo puedes ver el registro de la jornada de puertas abiertas, donde te contamos en detalle sobre el programa del curso, el proceso de aprendizaje y respondemos preguntas de potenciales alumnos:





Lee mas






All Articles