Виды аллокаторов из стандартной библиотеки

Изучаем готовые аллокаторы стандартной библиотеки и когда какой брать.

Arena-аллокатор выделяет память, но не освобождает её по кусочкам — вся выделенная память возвращается разом в конце; это быстро и удобно для пакетных задач с общим временем жизни.

Раз аллокатор передаётся явно, вызывающий волен выбрать стратегию под задачу. Стандартная библиотека Zig предлагает несколько готовых аллокаторов с разными компромиссами между скоростью, безопасностью и гибкостью.

GeneralPurposeAllocator — для разработки

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit(); // при завершении сообщит об утечках
    const alloc = gpa.allocator();

    const buf = try alloc.alloc(u8, 16);
    defer alloc.free(buf);
}

GeneralPurposeAllocator (часто сокращают до GPA) — универсальный аллокатор для отладки. Его суперспособность — детекция утечек: при deinit он сообщит, если что-то не освобождено, и даже покажет, где это выделили. Это бесценно для поиска утечек ещё на этапе разработки.

ArenaAllocator — выделяй много, освобождай разом

var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // одним вызовом освобождает ВСЁ
const alloc = arena.allocator();

const a = try alloc.alloc(u8, 100);
const b = try alloc.alloc(u8, 200);
// не нужно free для a и b — arena.deinit() освободит обоих

Arena-аллокатор идеален, когда множество выделений живут одинаково долго: распарсили файл, поработали, всё выбросили. Вы не освобождаете каждый кусок отдельно — один arena.deinit() возвращает всю память. Это и быстрее (нет учёта каждого блока), и проще (невозможно забыть отдельный free).

FixedBufferAllocator — без кучи вообще

var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const alloc = fba.allocator();
// выделяет внутри buffer; куча не задействована

FixedBufferAllocator раздаёт память из заранее выделенного буфера на стеке. Куча не используется вовсе — это критично для встраиваемых систем без динамической памяти. Когда буфер кончается, выделение возвращает ошибку.

Как работает под капотом

Все эти аллокаторы реализуют один и тот же интерфейс std.mem.Allocator, поэтому ваш код их не различает — вы лишь передаёте разный объект. Arena внутри держит список крупных блоков и режет их на запросы, освобождая всё одним махом. GPA ведёт учёт каждого выделения, что и позволяет ловить утечки, но добавляет накладные расходы — поэтому в релизе берут аллокатор быстрее.

Частые ошибки

Первая — использовать GPA в продакшене ради безопасности и терять на его учёте; для релиза берут более быстрый аллокатор. Вторая — звать отдельный free для памяти из арены: это лишнее, всё освободит deinit. Третья — забыть defer gpa.deinit() и не узнать об утечках.

Итог

  • Вызывающий выбирает стратегию выделения, передавая нужный аллокатор.
  • GeneralPurposeAllocator ловит утечки — идеален для разработки.
  • ArenaAllocator освобождает всё разом через deinit — для общего времени жизни.
  • FixedBufferAllocator раздаёт память из буфера без обращения к куче.
Проверьте себя
1. В чём главная польза GeneralPurposeAllocator при разработке?
AОн самый быстрый
BОн детектирует утечки памяти и сообщает о них
CОн не требует free
DОн работает без кучи
2. Нужно ли освобождать каждый блок памяти из ArenaAllocator по отдельности?
AДа, обязательно
BНет, arena.deinit() освобождает всю выделенную память разом
CТолько крупные блоки
DТолько в release-сборке