Ответ
Кастомные аллокаторы в C++ позволяют переопределить стратегию выделения памяти для стандартных контейнеров. Это полезно для оптимизации производительности, например, при частых выделениях мелких объектов одного типа.
Пример: аллокатор на основе пула памяти для std::vector
#include <memory>
#include <vector>
#include <cstdlib>
template <typename T>
class PoolAllocator {
private:
struct Block {
Block* next;
};
Block* freeList = nullptr;
static const size_t POOL_SIZE = 1024;
void allocatePool() {
char* rawMemory = static_cast<char*>(std::malloc(POOL_SIZE * sizeof(T)));
for (size_t i = 0; i < POOL_SIZE; ++i) {
Block* block = reinterpret_cast<Block*>(rawMemory + i * sizeof(T));
block->next = freeList;
freeList = block;
}
}
public:
using value_type = T;
PoolAllocator() {
allocatePool();
}
template <typename U>
PoolAllocator(const PoolAllocator<U>&) {}
T* allocate(std::size_t n) {
if (n != 1 || freeList == nullptr) {
// Fallback для нестандартных запросов
return static_cast<T*>(std::malloc(n * sizeof(T)));
}
Block* block = freeList;
freeList = freeList->next;
return reinterpret_cast<T*>(block);
}
void deallocate(T* p, std::size_t n) {
if (n == 1) {
Block* block = reinterpret_cast<Block*>(p);
block->next = freeList;
freeList = block;
} else {
std::free(p);
}
}
};
int main() {
// Использование аллокатора с вектором
std::vector<int, PoolAllocator<int>> vec;
for (int i = 0; i < 100; ++i) {
vec.push_back(i); // Выделения происходят из предварительно созданного пула
}
return 0;
}
Когда это нужно:
- Снижение фрагментации: Все объекты одного типа выделяются в непрерывном регионе.
- Повышение локальности данных: Объекты, используемые вместе, находятся рядом в памяти.
- Оптимизация производительности: Избегание частых вызовов системного
malloc/new. - Специализированные хранилища: Работа с разделяемой памятью (shared memory) или памятью GPU.
Ответ 18+ 🔞
Да ты посмотри, какие приколы бывают в C++! Вот сидишь, пишешь код, и вдруг понимаешь — стандартный аллокатор памяти работает как пьяный сантехник: то память выделит где попало, то фрагментация такая, что хоть святых выноси. А если ты делаешь высоконагруженную систему, где каждую микросекунду надо выжимать, то эти вечные вызовы malloc начинают бесить, как мартышлюшка под ухом.
Вот тут-то и приходят на помощь кастомные аллокаторы. По сути, это твоя личная стратегия раздачи памяти. Хочешь — выделяй всё заранее одним куском, хочешь — из специального пула тащи. Главное, что стандартные контейнеры вроде std::vector или std::list могут работать с твоим аллокатором, если он правильно написан. Ёпта, красота же!
Смотри, вот пример аллокатора, который не бегает к системе за каждой копейкой, а держит свой собственный бассейн памяти для объектов одного типа. Выглядит, конечно, как хитрая жопа, но идея простая.
#include <memory>
#include <vector>
#include <cstdlib>
template <typename T>
class PoolAllocator {
private:
// Это у нас будет связный список свободных блоков
struct Block {
Block* next;
};
Block* freeList = nullptr; // Голова списка свободных кусков
static const size_t POOL_SIZE = 1024; // Сколько объектов заготавливаем за раз
// Функция, которая набирает целый бассейн памяти и нарезает его на блоки
void allocatePool() {
// Выделяем память одним махом. Овердохуища сразу для 1024 объектов!
char* rawMemory = static_cast<char*>(std::malloc(POOL_SIZE * sizeof(T)));
// Теперь нарезаем этот большой кусок на отдельные квартирки и складываем в список свободных
for (size_t i = 0; i < POOL_SIZE; ++i) {
Block* block = reinterpret_cast<Block*>(rawMemory + i * sizeof(T));
block->next = freeList; // Прицепляем новый блок в начало списка
freeList = block;
}
}
public:
using value_type = T; // Обязательное объявление для совместимости
// При создании аллокатора сразу заготавливаем пул
PoolAllocator() {
allocatePool();
}
// Конструктор копирования для другого типа (нужно для ребинда в контейнерах)
template <typename U>
PoolAllocator(const PoolAllocator<U>&) {}
// Самое главное — функция выделения памяти
T* allocate(std::size_t n) {
// Если запросили не один объект, или пул пуст — идём в стандартный malloc (фолбэк)
if (n != 1 || freeList == nullptr) {
return static_cast<T*>(std::malloc(n * sizeof(T)));
}
// Достаём первый свободный блок из списка
Block* block = freeList;
freeList = freeList->next; // Передвигаем голову списка
return reinterpret_cast<T*>(block); // Возвращаем указатель, приведённый к нужному типу
}
// Функция освобождения памяти
void deallocate(T* p, std::size_t n) {
if (n == 1) {
// Если освобождаем один объект — просто возвращаем его в наш список свободных блоков
Block* block = reinterpret_cast<Block*>(p);
block->next = freeList;
freeList = block;
} else {
// Иначе отдаём память системе
std::free(p);
}
}
};
int main() {
// А вот и магия: создаём вектор, но говорим ему использовать наш PoolAllocator для int'ов
std::vector<int, PoolAllocator<int>> vec;
for (int i = 0; i < 100; ++i) {
vec.push_back(i); // Первые 100 выделений возьмутся из нашего предзаготовленного пула, быстро и без фрагментации!
}
return 0;
}
Ну и зачем весь этот геморрой, спросишь?
- Чтоб не было фрагментации, как в голове у алкаша. Все объекты
intв примере выше будут лежать рядышком в одном непрерывном куске памяти, который мы выделили заранее. Системная куча не превратится в швейцарский сыр. - Локальность данных — это сила. Если объекты лежат рядом, процессор их в кэш загружает пачками, и обращение к ним становится в разы быстрее. Не надо скакать по всей оперативке.
- Скорость, ёбать колотить! Вызов
mallocилиnew— это относительно тяжёлая операция, там могут быть блокировки, поиск свободного куска. А наш аллокатор из пула выдаёт память за O(1), просто перекинув пару указателей. Для высокочастотных выделений — просто праздник. - Для особых случаев. Допустим, тебе надо работать с shared memory (разделяемой памятью) или выделять память на видеокарте (GPU). Стандартный
newэтого не умеет, а свой аллокатор — запросто. Написал логику работы с нужным регионом памяти — и контейнеры твои там спокойно живут.
Короче, инструмент мощный, но и опасный. Если накосячишь в логике аллокатора, будет тебе не хиросима, а настоящий нигерсраки — падение, утечки, повреждение кучи. Но когда надо выжать из железа всё, без таких трюков никуда. Сам от себя охуеешь, насколько быстрее всё работать начинает.