Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SlabAllocator

Быстрый пул-аллокатор блоков фиксированного размера ("slab") для C++, плюс SlabMemoryResourcestd::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() на каждый объект.
  • SlabMemoryResourcestd::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.

Прямое использование SlabAllocator

#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 и явный вызов деструктора, как выше).

SlabMemoryResource со стандартными std::pmr-контейнерами

#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 с explicit align — можно.
  • Сравнительные бенчмарки — один прогон на одной машине, не статистически строгий результат. 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. Перед тем как опираться на конкретные цифры — перепроверить на целевом железе.

About

Быстрый пул-аллокатор блоков фиксированного размера ("slab") для C++, плюс SlabMemoryResource — std::pmr::memory_resource поверх него, чтобы подключить пул к std::pmr-контейнерам (list/map/set/unordered_*/ vector/deque/string) без написания собственного std::allocator.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages