Стандарт C++17 представил новый механизм управления памятью через пространство имен std::pmr (Polymorphic Memory Resources). Центральным элементом этой системы является абстрактный базовый класс std::pmr::memory_resource, который предоставляет полиморфный интерфейс для выделения и освобождения памяти.
Этот механизм позволяет:
- Легко настраивать стратегии управления памятью
- Использовать разные аллокаторы для разных частей программы
- Создавать каскадные системы выделения памяти
- Реализовывать специализированные менеджеры памяти
Базовый интерфейс memory_resource
namespace std::pmr {
class memory_resource {
public:
virtual ~memory_resource() = default;
void* allocate(size_t bytes, size_t alignment = alignof(max_align_t));
void deallocate(void* p, size_t bytes, size_t alignment = alignof(max_align_t));
bool is_equal(const memory_resource& other) const noexcept;
protected:
virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
virtual void do_deallocate(void* p, size_t bytes, size_t alignment) = 0;
virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
};
}
Стандартные реализации memory_resource
C++17 предоставляет несколько готовых реализаций memory_resource:
new_delete_resource
Использует глобальные операторы new и delete (аналог std::allocator).
std::pmr::memory_resource* res = std::pmr::new_delete_resource();
null_memory_resource
Генерирует исключение std::bad_alloc при любой попытке выделения памяти.
std::pmr::memory_resource* res = std::pmr::null_memory_resource();
synchronized_pool_resource
Потокобезопасный аллокатор на основе пулов памяти.
unsynchronized_pool_resource
Несинхронизированный (для использования в одном потоке) аллокатор на основе пулов.
monotonic_buffer_resource
Быстрый, но не возвращающий память аллокатор, выделяющий память из предоставленного буфера.
Пример использования monotonic_buffer_resource
#include <memory_resource>
#include <vector>
#include <iostream>
int main() {
// Буфер на стеке для аллокатора
char buffer[1024];
// Создаем monotonic_buffer_resource, использующий наш буфер
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
// Создаем вектор, использующий наш memory_resource
std::pmr::vector<int> vec{&pool};
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "Использовано памяти: " << pool.bytes_allocated()
<< " из " << pool.bytes_available() << "\n";
}
// При выходе из области видимости память автоматически освободится
}
Создание собственного memory_resource
Вы можете создать собственный аллокатор, унаследовавшись от memory_resource:
class CustomMemoryResource : public std::pmr::memory_resource {
protected:
void* do_allocate(size_t bytes, size_t alignment) override {
std::cout << "Выделение " << bytes << " байт\n";
return ::operator new(bytes, std::align_val_t{alignment});
}
void do_deallocate(void* p, size_t bytes, size_t alignment) override {
std::cout << "Освобождение " << bytes << " байт\n";
::operator delete(p, bytes, std::align_val_t{alignment});
}
bool do_is_equal(const memory_resource& other) const noexcept override {
return this == &other;
}
};
int main() {
CustomMemoryResource custom_res;
std::pmr::vector<int> vec(&custom_res);
vec.push_back(42);
}
Цепочки memory_resource
Одна из мощных возможностей — создание цепочек memory_resource:
#include <memory_resource>
int main() {
// Первичный аллокатор (монотонный буфер)
char buffer[4096];
std::pmr::monotonic_buffer_resource primary_pool{std::data(buffer), std::size(buffer)};
// Вторичный аллокатор (пул) использует первичный, когда ему нужно больше памяти
std::pmr::unsynchronized_pool_resource pool_alloc{&primary_pool};
// Контейнер использует пул-аллокатор
std::pmr::vector<int> vec{&pool_alloc};
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
// Память сначала берется из buffer, затем (если нужно) из кучи через primary_pool
}
Преимущества std::pmr::memory_resource
- Полиморфизм: Единый интерфейс для разных стратегий аллокации
- Гибкость: Легко комбинировать разные аллокаторы
- Производительность: Специализированные аллокаторы могут быть значительно быстрее
- Контроль: Точное управление распределением памяти
- Совместимость: Работает со всеми pmr-контейнерами (std::pmr::vector, std::pmr::string и др.)
Недостатки и ограничения
- Сложность: Более сложный API по сравнению с std::allocator
- Накладные расходы: Виртуальные вызовы могут снижать производительность в некоторых случаях
- Только C++17 и новее: Не доступен в более старых стандартах
Практические примеры использования
Выделение памяти из заранее подготовленного буфера
void processData() {
std::byte buffer[1'000'000];
std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
std::pmr::vector<std::pmr::string> strings{&pool};
strings.reserve(100);
// Все строки и вектор используют наш буфер
for (int i = 0; i < 100; ++i) {
strings.emplace_back("Пример строки");
}
// При выходе из функции вся память автоматически "освобождается"
}
Пул аллокаторов для объектов одного типа
struct GameObject {
// данные объекта...
};
void gameLoop() {
std::pmr::unsynchronized_pool_resource game_object_pool;
std::pmr::vector<GameObject*> objects;
for (int i = 0; i < 1000; ++i) {
auto obj = new (game_object_pool.allocate(sizeof(GameObject))) GameObject();
objects.push_back(obj);
}
// ... игровая логика ...
// Освобождение всех объектов
for (auto obj : objects) {
obj->~GameObject();
game_object_pool.deallocate(obj, sizeof(GameObject));
}
}
Заключение
std::pmr::memory_resource представляет собой мощный инструмент для управления памятью в современных программах на C++. Он особенно полезен в сценариях, где:
- Требуется высокая производительность при работе с памятью
- Необходим контроль над стратегиями выделения памяти
- Важно минимизировать фрагментацию памяти
- Работа ведется с большим количеством мелких объектов
Использование memory_resource позволяет создавать специализированные системы управления памятью, сохраняя при этом совместимость со стандартными контейнерами C++.
