SafeAllocator bucket retention #
checked against zig 0.17.0.
in 0.17, std.process.Init.gpa is a SafeAllocator in Debug and ReleaseSafe
(in ReleaseFast and ReleaseSmall it is c_allocator when libc is linked).
choosing ReleaseSafe therefore changes more than the generated safety checks:
it also picks a diagnostic allocator, unless the program brings its own.
small allocations fill buckets linearly, and freeing one does not make its slot available to another allocation. a bucket is released only once it is no longer being filled and every allocation in it is freed, so one small retained object keeps the rest of its bucket alive.
mixed lifetimes make that expensive. a stream processor parses a frame into temporary allocations, copies a short identifier into a long-lived set, and frees the parse. the identifier can pin a bucket of mostly freed parsing storage, and many unique identifiers pin many buckets: resident memory far above the live data, with every temporary allocation correctly freed.
for sustained production allocation, choose the allocator explicitly. a
libc-linked program can use std.heap.c_allocator in release builds and keep
init.gpa in Debug; ReleaseSafe's generated checks stay on either way, though
allocator diagnostics differ. separating temporary and retained lifetimes also
helps, but measure it with the real workload rather than assuming an arena
fixes retention in its backing allocator.
measure maximum RSS across matched workloads, not just logical live bytes; low logical totals do not rule out bucket retention. the same allocator's freed-memory poisoning is in 0.17 silent changes.
sources #
- zig 0.17.0
lib/std/start.zig(use_safe_allocator,callMain);lib/std/heap/SafeAllocator.zig(allocBucket,free) - relay
docs/incidents/2026-10-18-zlay-deploy/relay-eval/, 2026-10-18: two-source 60-second aarch64 linux runs peaked at 346,024 KiB with the default allocator and 8,636 KiB with libc; sources and durations matched, live events differed