Enzo Soares
Publicado em 25 de Março de 2025 às 13:58
Oi, amigo!
Na última postagem falamos de como a memória de um programa de computador funciona, em seus tipos: stack e heap. Essa segunda é o tema sobre o qual vos escrevo hoje. Em linguagens de programação de baixo nível, o gerenciamento de memória é feito através de uma API que abstrai as chamadas de sistema, permitindo ao usuário alocar e liberar memória. Em C, essa API é representada pelas chamadas malloc e free.
#include <stdio.h>
#include <stdlib.h> // Required for malloc and free
int main() {
int *ptr;
int n = 5;
// Reserva memória para 5 inteiros
ptr = (int*)malloc(n * sizeof(int));
if (ptr == NULL) {
printf("Alocação falhou!\n");
return 1; // Exit with error
}
// Armazena valores
for (int i = 0; i < n; i++) {
ptr[i] = i * 10;
}
// Imprime os valores
printf("Valores armazenados:\n");
for (int i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// Libera a memória
free(ptr);
return 0;
}
No exemplo acima, caso o programador esqueça de chamar free(ptr);, a memória de ptr nunca vai ser liberada. A gravidade desse vazamento de memória está no fato de que esse problema é silencioso. Você só percebe o problema quando a sua aplicação consome toda a memória disponível e nada te acusa qual linha é a culpada.
Zig, por outro lado, te oferece várias implementações dessa API, na forma de allocators. É responsabilidade do programador entender qual a melhor para o seu caso de uso.
O GPA, ou General Purpose Allocator, é o allocador que deve ser usado como padrão. Ele tem um equilíbrio entre performance e segurança, prevenindo vazamentos de memória, free duplo e uso depois de free.
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
_ = try allocator.alloc(u8, 100);
_ = gpa.deinit();
}
Como a memória reservada por allocator.alloc(u8, 100); não é liberada, gpa.deinit(); lança um erro, acusando o vazamento de memória. Para remover esse erro, é só liberar a memória:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const bytes = try allocator.alloc(u8, 100);
allocator.free(bytes);
_ = gpa.deinit();
}
O Arena Allocator é extremamente rápido, mas só permite a liberação de toda a memória reservada simultâneamente. Ele não tem os dispositivos de seguranaça do GPA. Ao chamar o método deinit(), toda a memória reservada pelo allocador é liberada simultâneamente.
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const allocator = arena.allocator();
_ = try allocator.alloc(u8, 1);
_ = try allocator.alloc(u8, 10);
_ = try allocator.alloc(u8, 100);
arena.deinit();
}
allocator.free(), para o Arena Allocator, é um noop.
O Fixed Buffer Allocator, embora tenha allocator no nome, não faz nenhuma reserva de memória no heap: ele recebe como argumento um buffer da stack e lá coloca toda a memória. Ele é útil quando o uso do heap não é desejado:
pub fn main() !void {
var buffer: [1000]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const memory = try allocator.alloc(u8, 100);
allocator.free(memory);
}
É alocador de testes tem o máximo possível de funcionalidades de segurança e pode apenas ser usado em testes:
const std = @import("std");
test "test" {
var array = std.ArrayList(i32).init(std.testing.allocator);
const expected: i32 = 42;
try array.append(expected);
try std.testing.expectEqual(expected, array.items[0]);
array.deinit();
}
Talvez a melhor funcionalidade do Zig para lidar com gerenciamento de memória é a palavra defer. Ela permite executar um bloco de código ao final do bloco atual:
const std = @import("std");
const print = std.debug.print;
pub fn main() !void {
defer print("2\n", .{});
print("1\n", .{});
}
O código acima escreve no terminal 1 e depois 2, apesar da ordem onde foram declaradas. Mas como isso pode nos ajudar a evitar vazamentos de memória? Em vez de precisar lembrar de liberar manualmente o recurso, você pode adicionar uma instrução defer logo após a instrução que aloca o recurso.
const std = @import("std");
test "defer" {
var array = std.ArrayList(i32).init(std.testing.allocator);
defer array.deinit();
const expected: i32 = 42;
try array.append(expected);
try std.testing.expectEqual(expected, array.items[0]);
}
test "sem defer" {
var array = std.ArrayList(i32).init(std.testing.allocator);
const expected: i32 = 42;
try array.append(expected);
try std.testing.expectEqual(expected, array.items[0]);
array.deinit();
}
O primeiro teste é muito mais fácil de validar, já que a reserva e a liberação de memória são declaradas juntas.
const std = @import("std");
test "defer" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
_ = try allocator.alloc(u8, 1);
_ = try allocator.alloc(u8, 10);
_ = try allocator.alloc(u8, 100);
}
test "sem defer" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const allocator = arena.allocator();
_ = try allocator.alloc(u8, 1);
_ = try allocator.alloc(u8, 10);
_ = try allocator.alloc(u8, 100);
arena.deinit();
}
Nesse exemplo acima, novamente, é muito mais simples de verificar que arena.deinit(); foi chamado. Imagine se o bloco
_ = try allocator.alloc(u8, 1);
_ = try allocator.alloc(u8, 10);
_ = try allocator.alloc(u8, 100);
tivesse na verdade 150 linhas?
Isso é tudo por hoje. Obrigado!