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::vector
con 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_resource
para 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::pmr
con 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 almemory_resource
que arroja una excepciónstd::bad_alloc
en 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 hilosynchronized_pool_resource
.class monotonic_buffer_resource : public std::pmr::memory_resource
Un solo subproceso, rápido ymemory_resource
de propósito especial toma memoria de un búfer preasignado, pero no la libera, es decir, solo puede crecer.
Ejemplo de uso
monotonic_buffer_resource
y 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_buffer
memoria 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_resource
medida 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::string
en su lugar std::string
.
Veamos un ejemplo con un tampón preasignados en la pila, lo que vamos a pasar como
memory_resource
a 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::string
mayor questd::string
. Esto se debe al hecho de que un puntero amemory_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::string
lugar 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_resource
y 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_resource
devuelve 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_resource
contenedores.
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