#
Аллокаторы в Zig версии 0.15.1
#
Что такое аллокаторы и зачем они нужны?
Аллокатор — это механизм управления памятью в программе. Его задача — выделять блоки памяти для нужд приложения и освобождать их после использования. Управление памятью напрямую связано с производительностью, и грамотный выбор аллокатора может значительно улучшить эффективность работы программы.
Основная цель аллокаторов — помогать программисту избежать утечек памяти, перерасхода ресурсов и проблем с производительностью. В большинстве языков программирования (таких как C++) управление памятью возложено на программиста, но Zig предоставляет разнообразные встроенные аллокаторы, которые решают многие задачи автоматически.
#
Аллокаторы в стандартной библиотеке Zig
Стандартная библиотека Zig включает несколько типов аллокаторов, предназначенных для различных ситуаций:
#
1. Arena Allocator
ArenaAllocator предназначен для краткосрочного хранения данных. Вся память выделяется один раз, и освобождение происходит централизованно в конце жизненного цикла арены. Подходит для ситуаций, когда вы уверены, что вся память будет освобождена разом (например, внутри функции или при выходе из блока). Хорошо подходит для императивных программ, например CLI-утилит.
const std = @import("std");
test "Arena" {
const ArenaAllocator = std.heap.ArenaAllocator;
var arena = ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const data = try allocator.alloc(u8, 1024);
std.debug.print("ArenaAllocator data ptr: {*}\n", .{data});
}
#
2. General Purpose Allocator
GeneralPurposeAllocator — это универсальный аллокатор, предназначенный для общего использования в приложениях. Он управляет памятью в "куче" и выделяет там блоки произвольного размера. Используется, когда нужно долговременное хранение данных и свободное размещение блоков памяти. Этот аллокатор автоматически отслеживает утечки памяти, информацию о которых можно получить в блоке defer при деинициализации. В версии 0.15.1 он помечен как устаревший и является синонимом для DebugAllocator, т.ч. скорее всего, использовать его для реальных приложений уже не следует (но это не точно, мы используем).
const std = @import("std");
test "GPA" {
var gpa_instance = std.heap.GeneralPurposeAllocator(.{}).init;
defer {
const deinit_status = gpa.deinit();
// неудачный тест: не удается выполнить попытку в defer,
// так как defer выполняется после того, как мы вернемся
if (deinit_status == .leak) std.testing.expect(false) catch @panic("GPA TEST FAIL");
}
const allocator = gpa_instance.allocator();
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
std.debug.print("GeneralPurposeAllocator data ptr:\t{*}\n", .{data});
}
#
3. Page Allocator
PageAllocator работает на уровне страниц памяти и предназначен для выделения больших блоков. Он полезен, когда требуется получение больших объемов памяти за один раз и освобождает от мелких операций аллокации. Этот аллокатор часто используется для передачи в другие, более высокоуровневые аллокаторы, например в ArenaAllocator или ThreadSafeAllocator.
const std = @import("std");
test "Page" {
const allocator = std.heap.page_allocator;
const data = try allocator.alloc(u8, 1024 * 1024);
defer allocator.free(data);
std.debug.print("PageAllocator data ptr: {*}\n", .{data});
}
#
4. std.testing.allocator
Аллокатор std.testing.allocator специально разработан для тестирования. Он ведет учет всех выделяемых и освобождаемых ресурсов, помогая отслеживать утечки памяти и прочие проблемы. Если попробовать использовать его в основном программном коде, то компилятор выдаст ошибку, что данный аллокатор предназначен только для использования в тестах.
Пример использования:
const std = @import("std");
test "Testing" {
const allocator = std.testing.allocator;
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
std.debug.print("TestingAllocator data ptr: {*}\n", .{data});
}
#
5. FixedBufferAllocator
FixedBufferAllocator представляет собой аллокатор, работающий с заранее выделенным буфером фиксированного размера. Он подходит для ситуаций, когда известно, какое количество памяти понадобится или доступно заранее, и нужно ограничить объем доступной приложению памяти. Например, это актуально при работе в микроконтроллерах и встроенных устройствах. Этот аллокатор не выделяет дополнительную память, а использует только предоставленный ему буфер.
Пример использования:
const std = @import("std");
test "Fixed" {
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
defer fba.reset();
const allocator = fba.allocator();
const data = try allocator.alloc(u8, 512);
defer allocator.free(data);
std.debug.print("FixedBufferAllocator data ptr: {*}\n", .{data});
}
#
6. ThreadSafeAllocator
ThreadSafeAllocator оборачивает собой любой другой аллокатор, делая его безопасным для использования в многопоточном режиме. Он защищает операцию выделения и освобождения памяти с помощью блокировки (lock), предотвращая одновременный доступ из разных потоков.
Пример использования:
const std = @import("std");
test "ThreadSafe" {
var gpa_instance = std.heap.GeneralPurposeAllocator(.{}).init;
defer _ = gpa_instance.deinit();
const unsafe_allocator = gpa_instance.allocator();
var ts_allocator: std.heap.ThreadSafeAllocator = .{
.child_allocator = unsafe_allocator,
};
const allocator = ts_allocator.allocator();
const data = try allocator.alloc(u8, 1024);
defer allocator.free(data);
std.debug.print("ThreadSafeAllocator data ptr: {*}\n", .{data});
}
#
Пример использования всех аллокаторов вместе
Ниже представлен пример, демонстрирующий совместное использование различных аллокаторов:
const std = @import("std");
test "Multi-allocator example" {
// Тестовый аллокатор
const test_alloc = std.testing.allocator;
const page_alloc = std.heap.page_allocator;
// ArenaAllocator
var arena = std.heap.ArenaAllocator.init(test_alloc);
defer arena.deinit();
const arena_alloc = arena.allocator();
// FixedBufferAllocator
var buffer: [1024]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
defer fba.reset();
const fb_allocator = fba.allocator();
// GeneralPurposeAllocator
var gpa_instance = std.heap.GeneralPurposeAllocator(.{}).init;
defer _ = gpa_instance.deinit();
const gp_allocator = gpa_instance.allocator();
// Thread-safe аллокатор поверх GP Allocator
var ts_instance: std.heap.ThreadSafeAllocator = .{
.child_allocator = gp_allocator,
};
const ts_allocator = ts_instance.allocator();
// Получаем данные из каждого аллокатора
const test_data = try test_alloc.alloc(u8, 100);
defer test_alloc.free(test_data);
const page_data = try page_alloc.alloc(u8, 100);
defer page_alloc.free(page_data);
const arena_data = try arena_alloc.alloc(u8, 100);
defer arena_alloc.free(arena_data);
const fb_data = try fb_allocator.alloc(u8, 100);
defer fb_allocator.free(fb_data);
const gp_data = try gp_allocator.alloc(u8, 100);
defer gp_allocator.free(gp_data);
const ts_data = try ts_allocator.alloc(u8, 100);
defer ts_allocator.free(ts_data);
std.debug.print("\nAllocated memories:\n", .{});
std.debug.print("- Test allocator: {*}\n", .{test_data});
std.debug.print("- Page allocator: {*}\n", .{page_data});
std.debug.print("- Arena allocator: {*}\n", .{arena_data});
std.debug.print("- FixedBufferAllocator: {*}\n", .{fb_data});
std.debug.print("- GeneralPurposeAllocator: {*}\n", .{gp_data});
std.debug.print("- ThreadSafeAllocator: {*}\n", .{ts_data});
}
#
Пример использования аллокатора в приложении
Рассмотрим небольшой пример, демонстрирующий использование аллокатора для работы с объектами:
const std = @import("std");
const MyStruct = struct {
id: u32,
name: []const u8,
};
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var obj = try allocator.create(MyStruct);
obj.id = 1;
obj.name = "First object";
std.debug.print("Created object: {any}\n", .{ obj });
}
В этом примере создается объект типа MyStruct, который сохраняется в памяти с помощью ArenaAllocator. За счет использования этого аллокатора, очистка памяти производится автоматически в конце жизненного цикла функции и вызывать метод destroy, нет необходимости.
#
Написание собственных аллокаторов
Иногда стандартных аллокаторов оказывается недостаточно, и возникает потребность в собственном механизме управления памятью. Пример разработки собственного аллокатора на Zig:
const std = @import("std");
pub const MyCustomAllocator = struct {
mem: []u8,
current_pos: usize,
const Self = @This();
pub fn init(mem: []u8) MyCustomAllocator {
return .{
.mem = mem,
.current_pos = 0,
};
}
pub fn alloc(self: *Self, n_bytes: usize, alignment: u29) ![]u8 {
const aligned_position = std.mem.alignForward(usize, self.current_pos, alignment);
if (aligned_position + n_bytes > self.mem.len) {
return error.OutOfMemory;
}
self.current_pos = aligned_position + n_bytes;
return self.mem[aligned_position..self.current_pos];
}
pub fn resize(self: *Self, old_mem: []u8, new_size: usize) ![]u8 {
_ = self;
_ = old_mem;
_ = new_size;
return error.UnsupportedOperation;
}
pub fn free(self: *Self, mem: []u8) void {
_ = self;
_ = mem;
}
};
test "Custom allocator" {
var buffer = std.mem.zeroes([1024]u8);
var custom_allocator = MyCustomAllocator.init(&buffer);
const alloc = custom_allocator.alloc(100, @alignOf(u32)) catch unreachable;
defer custom_allocator.free(alloc);
std.debug.print("Allocated memory length: {any}\n", .{alloc.len});
std.debug.print("Allocated memory ptr: {*}, contains: {any}\n", .{ alloc, alloc });
}
Этот пример иллюстрирует создание простого собственного аллокатора, управляющего памятью в определенном участке массива. Аллокатор распределяет память по запросу, перемещаясь по массиву и запоминая текущую позицию.
#
Заключение
В Zig существует богатый набор встроенных аллокаторов, каждый из которых предназначен для определенной задачи. Они различаются принципами работы и уровнем производительности, позволяя выбрать оптимальное решение для каждой ситуации:
- std.testing.allocator — для тестирования и отладки.
- Arena Allocator — для хранения данных, освобождаемых при деинициализации аллокатора.
- Page Allocator — для работы с большими блоками памяти.
- FixedBufferAllocator — для работы в условиях ограничения объема доступной памяти.
- GeneralPurposeAllocator — универсальный аллокатор для долгосрочного использования (помечен устаревшим в версии 0.15.1).
- ThreadSafeAllocator — обертка над любым аллокатором с защитой от конкуренции потоков.
Мы не затронули в этой статье еще ряд встроенных аллокаторов:
- MemoryPool — для работы с пулом памяти, который позволяет очень быстро распределять объекты одного типа. Используйте его, когда вам нужно выделить много объектов одного типа, поскольку он превосходит распределители общего назначения по производительности.
- SmpAllocator — распределитель памяти, разработанный для режима оптимизации
ReleaseFastс поддержкой многопоточности. - FailingAllocator — для для тестирования реакции на ошибку
OutOfMemory. Распределитель, выкидывает ошибку послеNвыделений памяти. - c_allocator — для использования совместно с
libc. - wasm_allocator — быстр, компактен и специфичен для использования в
WebAssembly.
Использование правильных аллокаторов и их комбинация позволит значительно повысить производительность и стабильность ваших приложений на Zig.