page_allocator granularity #
checked against zig 0.16.0 and 0.17.0.
std.heap.page_allocator rounds every non-zero request up to whole pages, so
small allocations through it that are not reliably freed leak a page each.
alloc(T, 0) costs nothing.
zero-length allocations are free #
Allocator short-circuits zero-byte requests before they reach the backing
allocator:
// std/mem/Allocator.zig, allocBytesWithAlignment
if (byte_count == 0) {
const ptr = comptime alignment.backward(math.maxInt(usize));
return @as([*]align(alignment.toByteUnits()) u8, @ptrFromInt(ptr));
}
the result is a synthetic aligned pointer: no mmap, no page. code does not
need contorting to avoid empty allocations.
every other request costs a page #
// std/heap/PageAllocator.zig
const page_aligned_len = mem.alignForward(usize, n, page_size);
a 72-byte request (alloc(Value, 3) with a 24-byte Value) takes a 4 KiB page
on most linux targets and 16 KiB on macOS arm64. wasting bytes is harmless;
losing a page several times a second is a leak that shows up within minutes.
the typical source is a protocol client that allocates small result trees from
page_allocator and frees them unreliably, for example array backings whose
leaves alias another buffer (per-command arena has
the redis case). three consumers each polling once a second and dropping one
array reply per poll lose about 12 KiB/s, which is close to 0.7 MiB a minute.
what to use instead #
- many small allocations with one lifetime: an
ArenaAllocatoroverpage_allocator, reset at the lifetime boundary (arena.reset(.retain_capacity)at each protocol command). the arena keeps and reuses its pages, so memory plateaus. since 0.16ArenaAllocatoris thread-safe when its child allocator is. - a general-purpose application allocator:
std.heap.smp_allocator, orc_allocatorwhen linking libc.page_allocatoris a substrate for other allocators, not one an application calls directly.
spotting it #
two series make it attributable: anonymous RSS (RssAnon in
/proc/self/status, which counts every mmap'd page) and an iteration counter
on the suspect loop. divide the RSS slope by the iteration rate over an idle
window; a result near a multiple of the page size is page-granular loss from
small allocations.
sources #
- zig 0.16.0 and 0.17.0
lib/std/mem/Allocator.zig(allocBytesWithAlignment),lib/std/heap/PageAllocator.zig(alignForwardto the page size) - 0.16.0 release notes: "heap.ArenaAllocator Becomes Thread-Safe and Lock-Free"
- prefect-server
docs/archive/leak-experiments.mdanddocs/archive/leak-investigation-plan.md: +0.62 MiB/min idle RSS from three redis broker consumers on alpine/musl