Hi everyone, I’m working on a Zig program that needs to interact with several C libraries. I’m a bit confused about the best practices for choosing allocators in this scenario, especially after reading some documentation.
The Zig documentation states: “Are you linking libc? In this case, std.heap.c_allocator is likely the right choice, at least for your main allocator.”
This sentence makes me wonder if I should indeed use std.heap.c_allocator as the primary allocator throughout my entire Zig program when linking libc.
Should std.heap.c_allocator be used for all allocations in my program if I’m linking libc? Or should I use other std.heap allocators (like std.heap.ThreadSafeAllocator, std.heap.FixedBufferAllocator, etc.) for suitable scenes in my Zig-native code, and only use c_allocator specifically for memory interactions directly with C libraries?
Does mixing allocators (using c_allocator for C interop and other Zig allocators for Zig-native code) lead to efficiency degradation? Or, conversely, would using Zig-native allocators for the majority of the Zig code actually improve overall efficiency compared to solely relying on c_allocator?
I’m looking for guidance on best practice for allocator selection in Zig programs that link C libraries.
ThreadSafeAllocator wraps another allocator of your choosing with a mutex to make it thread safe.
FixedBufferAllocator is an arena using a fixed size backing buffer, good when you have an upper limit on allocations + the arena behaviour.
c_allocator just wraps the allocator from libc, fast, thread safe. You use this when using c libs 1. so your code and the libraries can share the lifetime of allocations, ie you can use it to free memory allocated by c libraries and vice versa. 2. to prevent memory fragmentation from multiple main allocators.
fyi ArenaAllocator and ThreadSafeAllocator require a parent allocator to function.
Usually you pick one ‘root allocator’ at the start of your program and then use suballocators for specific needs, and (AFAIK) usually you’d use the std.heap.DebugAllocator as default since this gives you various important runtime safety checks (like use-after-free and leak checks).
I would use the std.heap.c_allocator as root allocator mostly in special situations (like when binary size matters a lot, or when running on exotic platforms like the web via wasm) - but then still use the DebugAllocator for debug-mode on native platforms.
…so that basically covers which root allocator to pick.
For sub-allocators it really depends. For instance think of the ArenaAllocator as more of a lifetime management system where you put allocations that all share the same maximum lifetime. You do many small allocations in an ArenaAllocator, and then ‘kill’ all allocations with a single call instead of freeing each allocation individually.
The FixedBufferAllocator is useful for small ad-hoc allocations when you know a max capacity upfront, or even when you just need a temporary allocator which lives on the stack instead of the heap.
Allocators are also composable, e.g. the ThreadSafeAllocator is just a thread-safety wrapper around another allocator.
I think the most important idea to ‘grok’ is to use ArenaAllocators for lifetime management. Instead of tracking the individual lifetime of thousands of individual objects, you reduce the problem to only a handful of ‘lifetime buckets’ - and that’s how manual memory management becomes ‘manageable’.
It’s the complete opposite of tracking individual lifetimes via reference counting or garbage collection.
Tbh, I would consider this poor design even in a C library (allocate inside the library but require the user to call free() directly - instead if the C library returns a allocated data it should also offer a function to free that data - and the icing on the cake is when the library allows to override the alloc/free functions).
That’s true. I do not need to use the free for my c library objects. But memory fragmentation is what I worry about. Does the documentation recommends using c_allocator primarily when linking linkc because mixing it with other allocators will cause memory fragmentation and reduce efficiency?
This is a bit of a tangential problem, and also depends a lot on how the suballocators are implemented. E.g. I haven’t checked, but would expect that the ArenaAllocator doesn’t pass each individual ‘small allocation’ through to the ‘parent allocator’ (which confusingly is called 'child allocator in the API) but instead allocates at least page-sized chunks from the parent allocator.
Also usually C stdlib allocators written in this century group small-size allocations into buckets to reduce fragmentation - and I would expect any Zig root allocator to do the same.
And finally, unless you are on WASM or an embedded device it’s unlikely that you need to worry much about fragmentation (in the sense that you might run out of address space because of fragmentation), this was mostly a problem with 32-bit address spaces and trivial early 20th century allocators which didn’t use size-buckets ![]()
Also, the best way to fix memory management issues is to not heap-allocate in the first place (but admittedly, with 3rd party C libraries you often don’t have much control about that).
Thanks. After learning more about allocators, I roughly understand what the document means. c_allocator is generally a faster choice than GPA (now renamed debug allocator). The disadvantage is that it needs to link libC, so if my program originally links libC, then using c_allocator is a very advantageous choice. But this seems to be challenged by SmpAllocator now. And DefaultAllocator seems likely to be added in the future to select different allocators in different compilation modes . Before it is officially introduced, the unified use of c_allocator seems to be a conservative and reasonable choice, which can get valgrind analysis of the program body consistent with the upstream library.
Another reason to use c_allocator whenever possible is this still unresolved issue, where the debug allocator still has some instability according to the latest comments. So if libc is linked, c_allocator should be the most predictable way to allocate at the moment.
I’m not sure if there is anything wrong with my understanding
This is incorrect, doing this will likely segfault. std.heap.c_allocator uses the libc memory allocator as the backing allocator, but it’s still a custom implementation on top of malloc/free that might (depending on the target) allocate more memory than requested, write some metadata to that chunk and run additional logic to ensure the returned pointer is correctly aligned even for alignments beyond alignof(max_align_t). You can’t allocate something with malloc() and free it with std.heap.c_allocator.free() or vice versa.
std.heap.raw_c_allocator on the other hand is implemented as direct calls to libc malloc/free, so it can technically support the “alloc in C, free in Zig” use case, but I agree with @floooh that such usage patterns are a bit dubious. Good C libraries will expose some kind of destroy/free function, and if they don’t you should probably just call std.c.free() directly to make your intent clear, instead of taking a needless detour through the allocator interface.
Just a general note, that information is (at least partially or potentially) outdated. I also stumbled upon some things there. From an answer to my questions on choice of memory allocator: