about things notes.zzstoatzz.io
notes languages ziglang memory page-allocator-granularity.md
2.9 kB · 64 lines
Markdown
at main

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 ArenaAllocator over page_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.16 ArenaAllocator is thread-safe when its child allocator is.
  • a general-purpose application allocator: std.heap.smp_allocator, or c_allocator when linking libc. page_allocator is 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 (alignForward to the page size)
  • 0.16.0 release notes: "heap.ArenaAllocator Becomes Thread-Safe and Lock-Free"
  • prefect-server docs/archive/leak-experiments.md and docs/archive/leak-investigation-plan.md: +0.62 MiB/min idle RSS from three redis broker consumers on alpine/musl