Последнее обновление:2026-10-12
PMR-ресурсы памяти в C++17¶
1.Введение¶
До C++17 стандартная библиотека предоставляла std::allocator, а в C++17 эта возможность была усилена появлением polymorphic_allocator.
До C++17, если мы хотели использовать std::allocator для создания собственного пула памяти, мы не могли применить традиционный подход с виртуальным полиморфизмом, поскольку std::allocator не предоставляет виртуальных функций. Поэтому наследование — очевидно плохой вариант, но мы можем использовать его как член класса, как показано ниже:
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_;
};
До C++17, если мы хотели выполнять какие-то операции print при выделении/освобождении памяти или какие-то пользовательские операции, можно было воспользоваться двумя способами:
- переопределить глобальные new/delete
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);
}
- написать собственный 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;
А какие ещё есть способы, помимо этого?
После C++17 мы можем использовать инструменты полиморфного управления памятью — polymorphic memory resources (pmr); способ применения:
#include <memory_resources>
pmr::monotonic_buffer_resource res(10);
pmr::vector<vector<int>> vec1(&res);
Для описанной выше задачи мы можем наследовать от memory_resource; пояснения к приведённому ниже коду смотрите ниже в разделе о 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);
}
};
Таким образом мы добились того же эффекта, что и выше.
2.memory_resource¶
Реализация класса std::par::memory_resource в STL довольно проста; ниже приведена реализация в виде псевдокода:
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;
};
В отличие от стандартного std::allocator, memory_resource предоставляет полиморфные возможности, позволяя пользователю через наследование переопределить следующие три интерфейса:
- do_allocate
- Выделяет память заданного размера и с заданным требованием выравнивания.
- do_deallocate
- Освобождает память, ранее выделенную через
do_allocate. - do_is_equal
- Проверяет, равен ли текущий объект ресурса памяти другому объекту ресурса памяти.
Поэтому наш приведённый выше TrackAllocator должен лишь переопределить эти три интерфейса функций.
Внимательные читатели заметят: помимо этих нескольких интерфейсов, memory_resource предоставляет ещё три интерфейса наружу. Кроме того, есть глобальные функции:
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
Возвращает указатель на memory_resource, который использует операторы new и delete для выделения и освобождения памяти. Этот ресурс реализован на основе глобальных операторов new и delete, поэтому он является менеджером памяти по умолчанию. Этот подкласс memory_resource — __resource_adaptor_imp, именно он отвечает за переопределение описанных выше трёх интерфейсов.
- null_memory_resource
Возвращает указатель на memory_resource, который представляет пустой ресурс памяти, не выполняющий никаких операций. Этот ресурс можно использовать, когда нужно протестировать что-то или сделать заглушку без фактического выделения памяти. В реализации в исходниках STL определён класс
inline memory_resource*
null_memory_resource() noexcept {
class type final : public memory_resource {};
}
type как подкласс переопределяет описанные выше три интерфейса; do_allocate бросает исключение ошибки выделения, поскольку по семантике выделение здесь не разрешено.
Поэтому при использовании null_memory_resource нужен try catch; например, string с длиной менее 16 символов размещается на stack — в этом случае наш memory_resource не задействуется и всё работает нормально, а при длине 16 и более будет выброшено исключение 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
Возвращает текущий глобальный менеджер ресурсов памяти по умолчанию. По умолчанию это тот же ресурс, который возвращает new_delete_resource(), но функция set_default_resource позволяет изменить ресурс по умолчанию.
- set_default_resource
Устанавливает менеджер ресурсов памяти; параметр может быть нулевым (возвращается ресурс по умолчанию), иначе ресурсом назначается переданный, а возвращается прежний ресурс по умолчанию.
3.Повторное использование памяти¶
std::pmr::monotonic_buffer_resource — это буфер, обладающий важными преимуществами в плане повторного использования памяти. monotonic_buffer_resource наследует memory_resource и переопределяет описанные выше три интерфейса; внутри он держит вышестоящий (upstream) memory_resource и, если текущей памяти недостаточно, берёт её у него. Это помогает снизить фрагментацию памяти и повысить коэффициент её использования.
Ниже перечислены некоторые способы использования std::pmr::monotonic.
Способ 1 — создание monotonic_buffer_resource с фиксированным буфером:
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};
Способ 2 — создание monotonic_buffer_resource на основе вышестоящего ресурса:
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);
}
}
Способ 3 — наследование от 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 — это потокобезопасный класс ресурса пула памяти. Он разработан для безопасного выделения и освобождения памяти в многопоточной среде. Когда несколько потоков одновременно пытаются выделить или освободить память, synchronized_pool_resource использует механизмы синхронизации для обеспечения потокобезопасности. Обычно это влечёт некоторые накладные расходы на производительность, так как требуется блокировка для защиты общего пула памяти.
Ему соответствует unsynchronized_pool_resource, для которого потокобезопасность должен обеспечивать сам пользователь.
Реализация synchronized_pool_resource в исходниках — это, по сути, mutex + unsynchronized_pool_resource.
class synchronized_pool_resource : public memory_resource {
public:
// some interface
private:
mutex __mut_;
unsynchronized_pool_resource __unsync_;
};
С помощью pool_options можно настраивать эти пулы памяти.
- max_blocks_per_chunk
Задаёт максимальное количество блоков, которое может содержаться в одном чанке. Используется для ограничения объёма памяти, получаемого пулом от вышестоящего ресурса памяти за один раз. Если установлено в 0, ядро через функцию munge_options изменит pool_options.
- largest_required_pool_block
Задаёт максимальный размер блока, который может обрабатывать пул памяти.
Похожие темы¶
- Атрибут [[nodiscard]] в C++17 | C++那些事
- std::string_view в C++17 | C++那些事
- Семнадцать расширенных возможностей C++17 | C++那些事
- Выведение аргументов шаблона класса CTAD в C++17 | C++那些事
- C++17 optional_ref | C++那些事
- std::variant в C++17 | C++那些事
- Структурированные привязки в C++17 | C++那些事
- Выведение auto в шаблонах C++17 | C++那些事
- Шаблоны переменных в C++17 | C++那些事