Última atualização:2026-10-12
Recursos de memória PMR no C++17¶
1.Introdução¶
Antes do C++17, a biblioteca padrão fornecia o std::allocator; no C++17, essa funcionalidade foi reforçada com a introdução do polymorphic_allocator.
Antes do C++17, se quiséssemos usar std::allocator para personalizar um pool de memória, não poderíamos usar a forma tradicional de polimorfismo virtual, porque std::allocator não fornece funções virtuais. Portanto, herdar claramente não é uma boa escolha, mas podemos usá-lo como membro de classe, como mostrado abaixo:
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 do C++17, se quiséssemos fazer algumas impressões ou operações personalizadas na alocação/liberação de memória, podíamos usar dois métodos:
- Personalizar o new/delete global
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 um 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;
Então, além desses, quais outras formas existem?
A partir do C++17, podemos usar as ferramentas de gerenciamento de memória polimórfico, os polymorphic memory resources (pmr); forma de uso:
#include <memory_resources>
pmr::monotonic_buffer_resource res(10);
pmr::vector<vector<int>> vec1(&res);
Para o problema acima, podemos herdar de memory_resource; a explicação do código abaixo está na seção memory_resource mais abaixo.
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);
}
};
Com isso, alcançamos o mesmo efeito de antes.
2.memory_resource¶
A implementação da classe std::par::memory_resource na STL é relativamente simples; a seguir, uma implementação em 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;
};
Diferente do std::allocator padrão, memory_resource oferece capacidade polimórfica, permitindo que o usuário, por meio de herança, reescreva as três interfaces a seguir:
- do_allocate
- Usada para alocar memória com tamanho e requisitos de alinhamento especificados.
- do_deallocate
- Libera a memória alocada anteriormente por
do_allocate. - do_is_equal
- Verifica se o objeto de recurso de memória atual é igual a outro objeto de recurso de memória.
Assim, o nosso TrackAllocator acima basta reescrever essas três interfaces de função.
Os mais atentos perceberão que, além dessas interfaces, memory_resource também fornece três interfaces voltadas para fora. Além disso, fornece interfaces globais:
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
Retorna um ponteiro de memory_resource que usa os operadores new e delete para alocar e liberar memória. Esse recurso é implementado com os operadores globais new e delete, sendo, portanto, o gerenciador de recursos de memória padrão. Essa subclasse de memory_resource é a __resource_adaptor_imp, responsável por reescrever as três interfaces acima.
- null_memory_resource
Retorna um ponteiro de memory_resource que representa um recurso de memória vazio que não executa nenhuma operação. Quando você quer testar ou fazer placeholder sem realizar alocação real de memória, pode usar esse recurso. Na implementação do código-fonte da STL, define-se um
inline memory_resource*
null_memory_resource() noexcept {
class type final : public memory_resource {};
}
type, como subclasse, reescreve as três interfaces acima; a interface do_allocate lança uma exceção de erro de alocação, porque a semântica é justamente não permitir alocação.
Por isso, quando usamos null_memory_resource, é preciso usar try catch; por exemplo: uma string com menos de 16 caracteres é alocada na stack, e nesse caso o nosso memory_resource aqui não é usado, então tudo roda normalmente; quando o comprimento é maior ou igual a 16, será lançada a exceção 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
Retorna o gerenciador global de recursos de memória padrão atual. Por padrão, é o mesmo recurso retornado por new_delete_resource(), mas o recurso padrão pode ser alterado pela função set_default_resource.
- set_default_resource
Define o gerenciador de recursos de memória; o parâmetro pode ser nulo (retorna o recurso padrão), caso contrário é definido como o recurso passado, e o recurso padrão anterior é retornado.
3.Reutilização de memória¶
std::pmr::monotonic_buffer_resource é um buffer que traz vantagens importantes na reutilização de memória. monotonic_buffer_resource herda de memory_resource, reescreve as três interfaces acima e mantém internamente um memory_resource upstream; se a memória atual for insuficiente, ele busca no upstream. Isso ajuda a reduzir a fragmentação de memória e a melhorar a taxa de utilização.
A seguir, algumas formas de uso relacionadas a std::pmr::monotonic.
Uso 1 - criar um monotonic_buffer_resource com um buffer fixo:
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 - criar um monotonic_buffer_resource aproveitando um recurso upstream:
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 - herdar 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 é uma classe de recurso de pool de memória thread-safe. Seu objetivo é realizar alocação e liberação de memória com segurança em ambientes multithread. Quando várias threads tentam, de forma concorrente, alocar ou liberar memória, o synchronized_pool_resource usa mecanismos de sincronização para garantir a segurança das threads. Isso normalmente traz alguma perda de desempenho, pois é necessário bloquear para proteger o pool de memória compartilhado.
O correspondente é o unsynchronized_pool_resource, que exige que o próprio usuário garanta a segurança das threads.
A implementação do código-fonte do synchronized_pool_resource é, na verdade, mutex + unsynchronized_pool_resource.
class synchronized_pool_resource : public memory_resource {
public:
// some interface
private:
mutex __mut_;
unsynchronized_pool_resource __unsync_;
};
pool_options pode ser usado para configurar esses pools de memória.
- max_blocks_per_chunk
Representa a quantidade máxima de blocos que cada chunk pode conter. É usada para limitar a quantidade de memória que o pool obtém de uma só vez do recurso de memória upstream. Se for definido como 0, o kernel modificará pool_options por meio da função munge_options.
- largest_required_pool_block
Representa o maior tamanho de bloco que o pool de memória consegue tratar.
Tópicos relacionados¶
- Atributo [[nodiscard]] no C++17 | C++那些事
- C++17 std::string_view | C++那些事
- Dezessete funcionalidades avançadas do C++17 | C++那些事
- Dedução de argumentos de template de classe CTAD no C++17 | C++那些事
- C++17 optional_ref | C++那些事
- C++17 std::variant | C++那些事
- Vinculações estruturadas no C++17 | C++那些事
- Dedução com auto em templates no C++17 | C++那些事
- Templates de variável no C++17 | C++那些事