Быстрый пул-аллокатор блоков фиксированного размера ("slab") для C++, плюс
SlabMemoryResource — std::pmr::memory_resource поверх него, чтобы
подключить пул к std::pmr-контейнерам (list/map/set/unordered_*/
vector/deque/string) без написания собственного std::allocator.
SlabAllocator — аллокатор, который выдаёт и переиспользует блоки одного
фиксированного размера с O(1) allocate()/deallocate(), выравниванием
и базовым overhead памяти порядка нескольких процентов. Один экземпляр класса
обслуживает ровно один размер блока (в этом смысле он не замена malloc, а
узкоспециализированный инструмент под конкретный тип/размер объекта).
Рассчитан на профиль нагрузки, где создаётся и уничтожается множество
мелких, примерно одинаковых по размеру, короткоживущих объектов (8–256
байт) — там, где обычный malloc/free даёт заметный overhead на
аллокацию и постепенный рост RSS процесса из-за фрагментации кучи.
SlabAllocator закрывает обе проблемы разом.
- O(1)
allocate()/deallocate()— без сканирования списка чанков и без обращения к системному аллокатору на каждый вызов. - Автоматическое выравнивание блоков по степени двойки.
- Возврат памяти ОС: чанк, полностью опустевший (кроме случая, когда он
единственный — тот остаётся "прогретым"), сразу освобождается через
free(), а не копится про запас. reset()— явный жёсткий сброс всего пула разом (все чанки освобождаются), для сценария "обработали транзакцию — выкинули всё вспомогательное состояние", без индивидуальныхdeallocate()на каждый объект.SlabMemoryResource—std::pmr::memory_resourceповерх набораSlabAllocator-пулов, сгруппированных по размерным классам; подключается к любомуstd::pmr-контейнеру через стандартныйstd::pmr::polymorphic_allocator<T>, без собственного класса-обёртки подstd::allocator.- Намеренно не потокобезопасен — подробности и практический вывод см. в "Известных ограничениях" ниже.
| Файл | Назначение |
|---|---|
include/SlabAllocator.h, src/SlabAllocator.cpp |
Сам аллокатор — основной компонент репозитория. SlabAllocator.h также содержит весь платформенный минимум (FORCE_INLINE, ASSERT, align_up<T>, os_alloc/os_free) |
include/SlabMemoryResource.h, src/SlabMemoryResource.cpp |
std::pmr::memory_resource поверх SlabAllocator — точка входа для std::pmr-контейнеров. |
tests/verify_slab_allocator.cpp |
Автономная программа проверки (без build-системы/фреймворка) — churn-тесты, регрессия на исправленные баги, reset(), SlabMemoryResource (с реальными std::pmr-контейнерами), выравнивание. |
benchmarks/bench_slab_allocator.cpp |
Автономные сравнительные бенчмарки SlabAllocator/SlabMemoryResource против malloc/unsynchronized_pool_resource/monotonic_buffer_resource/boost::pool<>; результаты и разбор — в docs/BENCHMARKS.md. |
docs/BENCHMARKS.md |
Сравнение с другими pool-аллокаторами — методология, сценарии и результаты бенчмарков против std::pmr::unsynchronized_pool_resource/monotonic_buffer_resource/boost::pool<>/голого malloc. |
Сборки нет — это исходники, которые подключаются в проект напрямую
(#include "SlabAllocator.h" с include/ в путях поиска заголовков,
компиляция src/SlabAllocator.cpp вместе с остальным проектом). Нужен
компилятор с поддержкой C++17.
#include <new>
#include "SlabAllocator.h"
struct Request
{
std::uint64_t id;
double amount;
};
SlabAllocator pool(sizeof(Request)); // один пул -- один размер блока
// SlabAllocator pool(sizeof(Request), alignof(Request)); // с выравниванием
void* raw = pool.allocate(); // O(1), может вернуть nullptr при OOM
Request* req = new (raw) Request{1, 2.5}; // placement new -- пул не строит объект сам
// ... работа с req ...
req->~Request();
pool.deallocate(req); // O(1)
// после "транзакции" -- одним вызовом выкинуть всё, что пул успел выделить:
pool.reset(); // жёсткий контракт: живых объектов быть не должноSlabAllocator — это низкоуровневый пул сырой памяти: он не вызывает
конструктор/деструктор объекта сам, это остаётся на вызывающем коде (через
placement new и явный вызов деструктора, как выше).
#include <list>
#include <map>
#include <string>
#include <vector>
#include "SlabMemoryResource.h"
SlabMemoryResource res; // одна ручка, которую держит вызывающий код
std::pmr::list<int> lst{&res};
lst.push_back(42);
std::pmr::map<int, std::string> m{&res};
m[1] = "hello";
std::pmr::vector<int> v{&res}; // не только node-based контейнеры
v.push_back(1);
std::pmr::string s{&res};
s = "small string";
// ...
res.reset(); // жёсткий контракт: живых объектов быть не должно;
// тот же объект res, что и выше -- ребиндинг STL не
// подменяет его, а значит reset() реально достаёт до
// памяти, которой пользовались все контейнеры вышеРазмер и выравнивание блока SlabMemoryResource получает из самого запроса
(bytes/alignment, которые ей передаёт std::pmr::polymorphic_allocator<T>)
— вручную ничего подбирать не нужно, и, в отличие от прежней схемы, работает
не только для node-based контейнеров: см. "Как это устроено" ниже.
Память выделяется чанками (Chunk) через os_alloc; стартовый размер чанка —
256 КБ, при исчерпании текущего размер следующего удваивается вплоть до
лимита, зависящего от размера блока. Внутри чанка свободные блоки образуют
интрузивный односвязный список прямо в своих же первых байтах (next_free)
— переиспользование блока не требует отдельной структуры данных. Чтобы
deallocate(ptr) за O(1) находил чанк, которому принадлежит ptr, каждый
2048-байтный "сектор" внутри чанка начинается со служебного указателя на
заголовок чанка (align_point_size); deallocate() просто округляет адрес
вниз до ближайшей границы 2048 байт и разыменовывает этот указатель — без
перебора чанков.
Сами чанки образуют двусвязный список: near_chunk_ — всегда голова
(откуда идёт выделение), tail_ — всегда актуальный хвост. Чанк, полностью
заполнившийся, уезжает в хвост; чанк, у которого только что освободился блок
после того, как он был полностью занят переставляется в голову
SlabMemoryResource сама не хранит блоки — она округляет размер запроса
вверх до ближайшей степени двойки (size class) и держит по одному
SlabAllocator на каждый встретившийся (size_class, align). Запросы, не
укладывающиеся в профиль (слишком большой размер или выравнивание,
приближающееся к 2048 байтам) — уходят в обычную (при необходимости
выровненную) аллокацию мимо пула. reset() — просто обход всех таких
пулов разом.
Build-системы нет; компилировать нужно вручную вместе с проектом,
использующим аллокатор. Для отладочных проверок (ASSERT)
определите _DEBUG:
g++ -std=c++17 -D_DEBUG -Iinclude -c src/SlabAllocator.cpp -o SlabAllocator.oОтдельная, не привязанная ни к какому фреймворку программа проверки —
tests/verify_slab_allocator.cpp. Она покрывает: регрессию на
near/tail-логики, рандомизированный churn-тест на уникальность выданных
блоков, контракт reset(), SlabMemoryResource
(с реальными std::pmr::list/map/vector/string, включая проверку,
что reset() реально опустошает память, которой они пользовались) и
выравнивание. Собрать и запустить:
g++ -std=c++17 -D_DEBUG -Wall -Wextra -fsanitize=address -Iinclude \
-o /tmp/verify tests/verify_slab_allocator.cpp src/SlabAllocator.cpp src/SlabMemoryResource.cpp && /tmp/verifyПри успехе программа печатает OK и завершается с кодом 0.
Сравнительные бенчмарки — benchmarks/bench_slab_allocator.cpp (команда
сборки и результаты — в заголовке файла и в docs/BENCHMARKS.md).
- Пулинг реально выигрывает только на маленьких, примерно одинаковых по
размеру объектах (профиль "8-256 байт"). Для
std::pmr::vector/std::pmr::string, которые растут удвоением и никогда не уменьшаются, каждая старая (отброшенная при реаллокации) ёмкость возвращается в свой bucket и лежит там до следующего запроса ровно такого же размера, который может не прийти вообще — не регрессия относительно жизни без пула, но и не безусловный выигрыш. - Диапазон размера блока не enforced.
block_size >= 8проверяется (ASSERT), а дляblock_size >= 512в debug-сборке безусловно срабатывает предупреждающийASSERT(false, ...)— аллокатор при этом продолжает работать корректно, порог просто не параметризован.SlabMemoryResourceиспользует границуsmall_max(по умолчанию 256) — запросы больше неё сразу уходят мимо пула, минуя эту проблему. - Не потокобезопасен. Ни
SlabAllocator, ниSlabMemoryResourceне рассчитаны на конкурентный доступ без внешней синхронизации — для потоковой локальности нужно самостоятельно объявитьthread_local SlabMemoryResource. - Выравнивание должно быть заметно меньше 2048 байт. Механизм поиска
чанка по указателю в
deallocate()полагается на то, что адрес ни одного выданного блока не совпадает ровно с границей 2048 байт — это неявно требуетalign < align_point_size.SlabMemoryResourceсама отсекает слишком большие выравнивания в fallback (см.fits_slab_profile()), так что через неё этот инвариант нарушить нельзя; при прямом использованииSlabAllocatorс explicitalign— можно. - Сравнительные бенчмарки — один прогон на одной машине, не
статистически строгий результат.
benchmarks/bench_slab_allocator.cppсравнивает сstd::pmr::unsynchronized_pool_resource/monotonic_buffer_resource/boost::pool<>/голымmalloc— итог неоднозначный: на churn-нагрузкеSlabMemoryResourceбыстрееunsynchronized_pool_resource, но заметно медленнееboost::pool<>; подробный разбор — вdocs/BENCHMARKS.md. Перед тем как опираться на конкретные цифры — перепроверить на целевом железе.