Saltar a contenido

Última actualización:2026-10-12

Recursos de memoria PMR en C++17

1.Introducción

Antes de C++17, la biblioteca estándar proporcionaba std::allocator; en C++17 esta función se reforzó con la introducción de polymorphic_allocator.

Antes de C++17, si queríamos usar std::allocator para personalizar un pool de memoria, no podíamos recurrir al enfoque polimórfico virtual tradicional, porque std::allocator no proporciona funciones virtuales. Por tanto, la herencia claramente no era una buena opción, pero podíamos usarlo como miembro de clase, como se muestra a continuación:

template <typename Allocator = std::allocator<uint8_t>>
class MemoryPool {
 public:
  explicit STLMemoryPool(const Allocator& alloc) : alloc_(alloc) {}

  Status Allocate(int64_t size, uint8_t** out) {
    try {
      *out = alloc_.allocate(size);
    } catch (std::bad_alloc& e) {
      return Status::OutOfMemory(e.what());
    }
    return Status::OK();
  }

  void Free(uint8_t* buffer, int64_t size) {
    alloc_.deallocate(buffer, size);
  }

 private:
  Allocator alloc_;
};

Antes de C++17, si queríamos hacer algunas operaciones de print al asignar/liberar memoria, o algunas operaciones personalizadas, había dos formas posibles:

  • Personalizar los new/delete globales
void* operator new(std::size_t size) {
  void* ptr = malloc(size);
  std::cout << "Allocated: " << size << " bytes at address " << ptr << std::endl;
  return ptr;
}

void operator delete(void* ptr, std::size_t n) noexcept {
  std::cout << "Deallocated: " << n << " bytes at address " << ptr << std::endl;
  free(ptr);
}
  • Personalizar el allocator
class allocator {
 public:
  using value_type = T;
  value_type* allocate(std::size_t n) {
    value_type* p = static_cast<value_type*>(::operator new(n * sizeof(value_type)));
    std::cout << "Allocated: " << n * sizeof(value_type) << " bytes at address "
              << static_cast<void*>(p) << std::endl;
    return p;
  }

  void deallocate(value_type* p, std::size_t n) noexcept {
    std::cout << "Deallocated: " << n * sizeof(value_type) << " bytes at address "
              << static_cast<void*>(p) << std::endl;
    ::operator delete(p);
  }
};

std::vector<int, allocator<int>> vec;

Entonces, aparte de esto, ¿qué otras formas hay?

A partir de C++17, podemos usar las herramientas de gestión polimórfica de memoria, los polymorphic memory resources (pmr); su forma de uso:

#include <memory_resources>

pmr::monotonic_buffer_resource res(10);
pmr::vector<vector<int>> vec1(&res);

Para el problema anterior, podemos heredar de memory_resource; la explicación del siguiente código se encuentra más abajo, en memory_resource.

class TrackAllocator : public std::pmr::memory_resource {
    void* do_allocate(std::size_t bytes, std::size_t alignment) override {
        void* p = std::pmr::new_delete_resource()->allocate(bytes, alignment);
        std::cout << "do_allocate: " << bytes << " bytes at " << p
                  << " alignment: " << alignment << std::endl;
        return p;
    }

    void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
      std::cout << "do_deallocate: " << bytes << " bytes at "
                << " alignment: " << alignment << std::endl;
      return std::pmr::new_delete_resource()->deallocate(p, bytes, alignment);
    }

    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return std::pmr::new_delete_resource()->is_equal(other);
    }
};

Con esto conseguimos el efecto mostrado arriba.

2.memory_resource

La implementación de la clase std::par::memory_resource en la STL es bastante sencilla; he aquí una implementación en pseudocódigo:

class memory_resource {
  static const size_t __max_align = alignof(max_align_t);

public:
  virtual ~memory_resource();

  void* allocate(size_t __bytes, size_t __align = __max_align) {
    return do_allocate(__bytes, __align);
  }

  void deallocate(void* __p, size_t __bytes, size_t __align = __max_align) {
    do_deallocate(__p, __bytes, __align);
  }

  bool is_equal(const memory_resource& __other) const noexcept { 
    return do_is_equal(__other); 
  }

private:
  virtual void* do_allocate(size_t, size_t)                       = 0;
  virtual void do_deallocate(void*, size_t, size_t)               = 0;
  virtual bool do_is_equal(memory_resource const&) const noexcept = 0;
};

A diferencia del std::allocator por defecto, memory_resource ofrece capacidad polimórfica, permitiendo al usuario, mediante herencia, redefinir las tres interfaces siguientes:

  • do_allocate
  • Se usa para asignar memoria del tamaño y con la alineación especificados.
  • do_deallocate
  • Libera la memoria asignada previamente mediante do_allocate.
  • do_is_equal
  • Comprueba si el objeto de recurso de memoria actual es igual a otro objeto de recurso de memoria.

Así, nuestro TrackAllocator de arriba basta con redefinir esas tres interfaces de funciones.

Quienes sean observadores habrán notado que, además de estas interfaces, memory_resource también ofrece tres interfaces hacia el exterior. Además, proporciona interfaces globales:

memory_resource* new_delete_resource() noexcept;
memory_resource* null_memory_resource() noexcept;
memory_resource* get_default_resource() noexcept;
memory_resource* set_default_resource(memory_resource* __r) noexcept;
  • new_delete_resource

Devuelve un puntero memory_resource que usa los operadores new y delete para asignar y liberar memoria. Este recurso se implementa con los operadores globales new y delete, por lo que es el gestor de recursos de memoria por defecto. Esta subclase de memory_resource es __resource_adaptor_imp, que se encarga de redefinir las tres interfaces anteriores.

  • null_memory_resource

Devuelve un puntero memory_resource que representa un recurso de memoria vacío que no ejecuta ninguna operación. Puedes usar este recurso cuando quieras hacer pruebas o reservar un lugar sin realizar asignaciones de memoria reales. La implementación en el código fuente de la STL define lo siguiente:

inline memory_resource*
null_memory_resource() noexcept {
    class type final : public memory_resource {};
}

Como subclase, type redefine las tres interfaces anteriores; la interfaz do_allocate lanzará una excepción de error de asignación, porque la semántica es justamente no permitir la asignación.

do_allocate(size_t, size_t) override
    { std::__throw_bad_alloc(); }

Por eso, cuando usamos null_memory_resource necesitamos try catch; por ejemplo: un string con menos de 16 caracteres se asigna en la pila, y en ese caso no se usa nuestro memory_resource, de modo que todo funciona con normalidad; cuando la longitud es mayor o igual que 16, se lanzará la excepción std::bad_alloc.

std::pmr::memory_resource* default_resource = std::pmr::null_memory_resource();

std::pmr::set_default_resource(default_resource);

try {
  std::pmr::vector<int> data{1};
} catch (std::bad_alloc e) {
  std::cerr << "bad_alloc is thrown vector" << std::endl;
}

std::pmr::string str1{"hello world hel"};
std::cerr << str1 << " length:" << str1.size() << std::endl;

try {
  std::pmr::string str1{"hello world hel0"};
} catch (std::bad_alloc e) {
  std::cerr << "bad_alloc is thrown string" << std::endl;
}
  • get_default_resource

Devuelve el gestor de recursos de memoria global actual. Por defecto es el mismo recurso que devuelve new_delete_resource(), pero mediante la función set_default_resource puede cambiarse el recurso por defecto.

  • set_default_resource

Establece el gestor de recursos de memoria; el parámetro puede ser nulo (se devuelve el recurso por defecto); en caso contrario se establece el recurso pasado y se devuelve el recurso por defecto anterior.

3.Reutilización de memoria

std::pmr::monotonic_buffer_resource es un búfer que ofrece ventajas importantes en la reutilización de memoria. monotonic_buffer_resource hereda de memory_resource y redefine las tres interfaces anteriores; internamente mantiene un memory_resource aguas arriba, y si la memoria actual es insuficiente, va a buscarla aguas arriba. Esto ayuda a reducir la fragmentación de memoria y a mejorar su aprovechamiento.

A continuación se listan algunas formas de usar std::pmr::monotonic.

Uso 1 - Crear un monotonic_buffer_resource con un búfer fijo:

char buf1[30] = {};
std::fill_n(std::begin(buf1), std::size(buf1) - 1, '_');
std::pmr::monotonic_buffer_resource pool1{std::data(buf1), std::size(buf1)};
std::pmr::vector<int> myVec1{&pool1};

Uso 2 - Crear un monotonic_buffer_resource aprovechando un recurso aguas arriba:

std::pmr::memory_resource* upstreamResource2 = std::pmr::get_default_resource();
std::pmr::monotonic_buffer_resource pool2(upstreamResource2);
{
    std::pmr::vector<double> numbers(&pool2);
    for (double d = 0.1; d < 1.0; d += 0.1) {
        numbers.push_back(d);
    }
}

Uso 3 - Heredar de std::pmr::monotonic_buffer_resource:

class MyMonotonicBufferResource : public std::pmr::monotonic_buffer_resource {
public:
    using std::pmr::monotonic_buffer_resource::monotonic_buffer_resource; // Inherit constructors

    // Public function to access do_allocate
    void* allocate(std::size_t bytes, std::size_t alignment) {
        return do_allocate(bytes, alignment);
    }

    // Public function to access do_deallocate
    void deallocate(void* p, std::size_t bytes, std::size_t alignment) {
        do_deallocate(p, bytes, alignment);
    }
};

MyMonotonicBufferResource pool3;
// Allocate memory
void* mem = pool3.allocate(100, alignof(int));

// Use the allocated memory (this is just an example)
std::pmr::vector<int> numbers({1, 2, 3}, &pool3);

// Deallocate memory
pool3.deallocate(mem, 100, alignof(int));

std::cout << "Usage 3 - Inherit: \n\n";

4.pool resource

synchronized_pool_resource es una clase de recurso de pool de memoria segura para hilos. Su propósito es realizar asignaciones y liberaciones de memoria de forma segura en entornos multihilo. Cuando varios hilos intentan concurrentemente asignar o liberar memoria, synchronized_pool_resource usa mecanismos de sincronización para garantizar la seguridad entre hilos. Esto normalmente conlleva cierto coste de rendimiento, porque hace falta bloquear para proteger el pool de memoria compartido.

Su contrapartida es unsynchronized_pool_resource, que exige que el propio usuario garantice la seguridad entre hilos.

La implementación de synchronized_pool_resource en el código fuente es, en realidad, mutex + unsynchronized_pool_resource.

class synchronized_pool_resource : public memory_resource {

public:
    // some interface
private:
  mutex __mut_;
  unsynchronized_pool_resource __unsync_;
};

pool_options sirve para configurar estos pools de memoria.

struct pool_options {
  size_t max_blocks_per_chunk        = 0;
  size_t largest_required_pool_block = 0;
};
  • max_blocks_per_chunk

Indica la cantidad máxima de bloques que puede contener cada chunk. Se usa para limitar la cantidad de memoria que el pool obtiene de una sola vez del recurso de memoria aguas arriba. Si se establece en 0, el núcleo modificará pool_options mediante la función munge_options.

  • largest_required_pool_block

Indica el tamaño de bloque máximo que el pool de memoria puede gestionar.

Temas relacionados

Comentarios