Memory
Like Zig, the C programming language has manual memory management. However, unlike Zig, C has a default allocator - , realloc
, and free
. When linking against libc, Zig exposes this allocator with std.heap.c_allocator
. However, by convention, there is no default allocator in Zig. Instead, functions which need to allocate accept an *Allocator
parameter. Likewise, data structures such as std.ArrayList
accept an *Allocator
parameter in their initialization functions:
allocator.zig
$ zig test allocator.zig
Test [1/1] test "using an allocator"...
All 1 tests passed.
In the above example, 100 bytes of stack memory are used to initialize a FixedBufferAllocator
, which is then passed to a function. As a convenience there is a global FixedBufferAllocator
available for quick tests at std.testing.allocator
, which will also do perform basic leak detection.
Zig has a general purpose allocator available to be imported with std.heap.GeneralPurposeAllocator
. However, it is still recommended to follow the Choosing an Allocator guide.
What allocator to use depends on a number of factors. Here is a flow chart to help you decide:
- Are you making a library? In this case, best to accept an
*Allocator
as a parameter and allow your library’s users to decide what allocator to use. - Are you linking libc? In this case, is likely the right choice, at least for your main allocator.
Is your program a command line application which runs from start to end without any fundamental cyclical pattern (such as a video game main loop, or a web server request handler), such that it would make sense to free everything at once at the end? In this case, it is recommended to follow this pattern:
cli_allocation.zig
$ zig build-exe cli_allocation.zig
$ ./cli_allocation
ptr=i32@7f00d058c018
When using this kind of allocator, there is no need to free anything manually. Everything gets freed at once with the call to
arena.deinit()
.- Are the allocations part of a cyclical pattern such as a video game main loop, or a web server request handler? If the allocations can all be freed at once, at the end of the cycle, for example once the video game frame has been fully rendered, or the web server request has been served, then
std.heap.ArenaAllocator
is a great candidate. As demonstrated in the previous bullet point, this allows you to free entire arenas at once. Note also that if an upper bound of memory can be established, thenstd.heap.FixedBufferAllocator
can be used as a further optimization. - Are you writing a test, and you want to make sure
error.OutOfMemory
is handled correctly? In this case, usestd.testing.FailingAllocator
. - Are you writing a test? In this case, use
std.testing.allocator
. - Finally, if none of the above apply, you need a general purpose allocator. Zig’s general purpose allocator is available as a function that takes a struct of configuration options and returns a type. Generally, you will set up one
std.heap.GeneralPurposeAllocator
in your main function, and then pass it or sub-allocators around to various parts of your application. - You can also consider .
test.zig
$ zig test test.zig
./docgen_tmp/test.zig:4:9: error: expected type '[]u8', found '*const [5:0]u8'
However if you make the slice constant, then it works:
strlit.zig
$ zig test strlit.zig
Test [1/1] test "string literal to constant slice"...
All 1 tests passed.
Just like string literals, const
declarations, when the value is known at comptime, are stored in the global constant data section. Also are stored in the global constant data section.
var
declarations inside functions are stored in the function’s stack frame. Once a function returns, any Pointers to variables in the function’s stack frame become invalid references, and dereferencing them becomes unchecked .
var
declarations at the top level or in struct declarations are stored in the global data section.
The location of memory allocated with allocator.alloc
or allocator.create
is determined by the allocator’s implementation.
TODO: thread local variables
Zig programmers can implement their own allocators by fulfilling the Allocator interface. In order to do this one must read carefully the documentation comments in std/mem.zig and then supply a reallocFn
and a shrinkFn
.
Heap Allocation Failure
Many programming languages choose to handle the possibility of heap allocation failure by unconditionally crashing. By convention, Zig programmers do not consider this to be a satisfactory solution. Instead, error.OutOfMemory
represents heap allocation failure, and Zig libraries return this error code whenever heap allocation failure prevented an operation from completing successfully.
Some have argued that because some operating systems such as Linux have memory overcommit enabled by default, it is pointless to handle heap allocation failure. There are many problems with this reasoning:
- Only some operating systems have an overcommit feature.
- Linux has it enabled by default, but it is configurable.
- Windows does not overcommit.
- Embedded systems do not have overcommit.
- Hobby operating systems may or may not have overcommit.
- For real-time systems, not only is there no overcommit, but typically the maximum amount of memory per application is determined ahead of time.
- When writing a library, one of the main goals is code reuse. By making code handle allocation failure correctly, a library becomes eligible to be reused in more contexts.
- Although some software has grown to depend on overcommit being enabled, its existence is the source of countless user experience disasters. When a system with overcommit enabled, such as Linux on default settings, comes close to memory exhaustion, the system locks up and becomes unusable. At this point, the OOM Killer selects an application to kill based on heuristics. This non-deterministic decision often results in an important process being killed, and often fails to return the system back to working order.
Recursion is a fundamental tool in modeling software. However it has an often-overlooked problem: unbounded memory allocation.
Recursion is an area of active experimentation in Zig and so the documentation here is not final. You can read a .
The short summary is that currently recursion works normally as you would expect. Although Zig code is not yet protected from stack overflow, it is planned that a future version of Zig will provide such protection, with some degree of cooperation from Zig code required.
It is the Zig programmer’s responsibility to ensure that a pointer is not accessed when the memory pointed to is no longer available. Note that a is a form of pointer, in that it references other memory.
In order to prevent bugs, there are some helpful conventions to follow when dealing with pointers. In general, when a function returns a pointer, the documentation for the function should explain who “owns” the pointer. This concept helps the programmer decide when it is appropriate, if ever, to free the pointer.
For example, the function’s documentation may say “caller owns the returned memory”, in which case the code that calls the function must have a plan for when to free that memory. Probably in this situation, the function will accept an *Allocator
parameter.
Sometimes the lifetime of a pointer may be more complicated. For example, the slice has a lifetime that remains valid until the next time the list is resized, such as by appending new elements.