STL-совместимый аллокатор памяти на C++17 с пулами блоков разного размера. Заранее резервирует крупные области памяти и выдаёт из них блоки, избегая частых обращений к системному new/delete. Размеры блоков задаются параметрами шаблона, поэтому конфигурация пулов фиксируется на этапе компиляции.
Header-only: достаточно подключить один заголовок.
Классический std::allocator под капотом каждый раз дёргает системный аллокатор. Для контейнеров с большим числом мелких аллокаций это дорого. Pool allocator решает это так:
- при создании выделяет несколько больших непрерывных пулов памяти;
- каждый пул разбит на блоки фиксированного размера;
- запрос на выделение обслуживается поиском нужного числа подряд идущих свободных блоков;
- освобождение просто помечает блоки свободными — без обращения к ОС.
Несколько пулов с разными размерами блоков позволяют эффективнее обслуживать запросы разного объёма: маленькие объекты идут в пул с мелкими блоками, крупные — в пул с большими.
#include "allocator.h"
#include <deque>
int main() {
// Пул на 100000 байт, блоки по 8 байт.
std::deque<int, MemoryPoolAllocator<int, 100000, 8>> dq;
for (int i = 0; i < 10000; ++i) {
dq.push_back(i);
}
}Несколько пулов разного размера блока (16, 32 и 64 байта):
std::vector<double, MemoryPoolAllocator<double, 100000, 16, 32, 64>> v;
v.reserve(1000);
for (int i = 0; i < 1000; ++i) v.push_back(i * 1.5);MemoryPoolAllocator<T, pool_size, first_block_size, block_sizes...>| Параметр | Назначение |
|---|---|
T |
Тип элемента (как у любого аллокатора) |
pool_size |
Размер каждого пула в байтах |
first_block_size |
Размер блока первого пула |
block_sizes... |
Размеры блоков остальных пулов (variadic) |
На каждый размер блока создаётся отдельный пул на pool_size байт.
Проект состоит из двух классов в MemoryPoolAllocator.h:
MemoryPool — владеет одной непрерывной областью памяти, разбитой на блоки фиксированного размера. Хранит битовую карту занятости и умеет выдавать несколько подряд идущих свободных блоков под запрос в байтах. Класс владеет сырой памятью, поэтому копирование запрещено, а перемещение реализовано (rule of five).
MemoryPoolAllocator — STL-совместимый аллокатор поверх вектора пулов. Реализует allocate / deallocate, rebind, operator==, необходимые typedef'ы. При выделении перебирает пулы и отдаёт память из первого подходящего; при освобождении находит пул-владелец по диапазону адресов.
Пулы хранятся под общим std::shared_ptr, поэтому копии аллокатора (а STL копирует аллокаторы, в том числе через rebind) разделяют одну память — это исключает двойное освобождение и use-after-free при перепривязке.
Аллокатор плохо подходит для std::vector. vector хранит данные в одном непрерывном буфере и при росте удваивает его, запрашивая всё больший непрерывный кусок. Как только нужный размер превысит pool_size, выделение завершится std::bad_alloc — ни один пул не сможет выдать столько подряд.
Например, для std::vector<int, MemoryPoolAllocator<int, 32000, 4, 8>> заполнение 5000 элементами упадёт: при росте capacity до 8192 элементов vector запросит 8192 × 4 = 32768 байт — больше, чем пул в 32000 байт.
Что с этим делать:
- использовать контейнеры, которые аллоцируют память кусками, а не одним буфером —
std::deque,std::list,std::map; - для
vector— вызыватьreserve()заранее, чтобы избежать удвоения (тогда единственный буфер должен помещаться в пул); - увеличить
pool_sizeтак, чтобы он вмещал пиковый буфер контейнера.
Выравнивание не учитывается. Блоки нарезаются побайтово, и возвращаемый указатель выровнен только по границе блока. Для типов с alignof(T), превышающим фактическое выравнивание блока (например, SIMD-типы), это формально некорректно. Для стандартных скалярных типов (int, double, указатели) при разумных размерах блоков работает корректно.
Header-only — отдельная сборка не требуется, достаточно подключить заголовок:
#include "allocator.h"Требуется компилятор с поддержкой C++17 (используются fold expressions). Проверено на GCC и Clang, собирается без предупреждений с -Wall -Wextra, проходит проверку AddressSanitizer и UBSanitizer на C++17 и C++20.
Пример сборки демо:
g++ -std=c++17 -Wall -Wextra main.cpp -o demo- Variadic-шаблоны для конфигурации произвольного числа пулов
- Fold expression (C++17) для создания пулов из пакета параметров на этапе компиляции
- Rule of five у
MemoryPool: запрет копирования,noexcept-перемещение (нужно, чтобыstd::vectorмог перемещать пулы при росте) std::shared_ptrдля разделяемого владения пулами между копиями аллокатора- Поиск свободной серии блоков по битовой карте; освобождение за O(1) по вычислению индекса из адреса