Hi all, Odin is my first exposure to manual memory management. When learning something I take notes as I go and then write these up into a single peice so it cements in my mind and I can refer back to it.
I would therefore appreciate it if anyone could read any or all of the following and correct me where I’ve gone wrong/misunderstood, or provide additional clarification to anything below? Appreciate your time, thank you
Memory
Areas of memory
Memory for our program lives in one of three conceptual areas:
-
The global space reserved for fixed data known at compile time, e.g. constants, procedures
-
The stack, where memory is allocated for variables within the scope of each procedure, and that memory is freed upon leaving the function automatically. This is why if you try and return a pointer to a variable defined in the scope of a procedure, you get warned about it (dangling pointer) - the stack memory it points to is no longer deterministic after the stack frame is wiped.
main :: proc()
{
b: ^int = mytest()
}
mytest :: proc() -> ^int {
a: int = 1
return &a
}
- The heap, where we allocate memory for data we want to persist beyond a single stack frame. Memory allocated here needs to be tracked so we can free it; memory that isn’t freed appropriately is referred to as a memory leak. Often this can go un-noticed since a bit of unused extra memory here and there won’t impact your program, but when you start allocating memory repeatedly (say in a loop, or because your program runs for a long time), then the memory usage of the program can grow and grow and the results are undesirable for obvious reasons.
Manual memory management
There are two main ways of manually allocating memory:
new
andfree
- For allocating memory for a given type, typically a single valuemake
anddelete
- For constructing a family of particular first-class data types:array
,slice
,dynamic
,map
Context
There is an implicit context
that gets passed into every function, and contains among other things:
context.allocator
- a general purpose heap allocator used to allocate and deallocate memory imperativelycontext.temp_allocator
- a growable “arena” allocator used for short-lived data. You build up your allocations and then callfree_all()
to deallocate them all in one fell swoop. This is very useful in game loops, for instance.
When we call something like new(i32)
or delete(myarr)
, it’s sugar for new(int, context.allocator)
etc. So, if we change the allocator we’re using by reassigning context.allocator
, we can still use new()
and it’ll respect the new allocator.
When do allocations happen?
We can either choose allocate memory on the stack, or the heap. Or sometimes, it’s a mixture of both!
// Allocate an int on the stack, zero-val'd. Gets automatically freed.
a: i32
// Allocate an int on the heap and return a pointer to it. Needs to be freed.
a := new(i32)
// Allocate an array on the stack. Gets automatically freed.
a := [3]int{1, 2, 3}
// Allocate an array on the heap, return pointer. Needs to be freed.
a := make([3]int, 3)
... etc
// Allocate a dynamic on the stack, but the data will be on heap. Thus it still needs to be freed by hand.
a := [dynamic]int{1,2,3}
// Allocate a dynamic on the heap, and the data is on the heap. Needs to be freed.
a := make([dynamic]int, 3)
// Same behaviour for dynamic applies to maps too
Allocation behaviour of first-class types
So in general, we can choose to allocate any first-class type on the stack or the heap, BUT dynamic
and map
have implicit allocations regardless of if we do:
a := [dynamic]int{1, 2, 3}
OR
a := make([dynamic]int, 3)
However, in either case, we need to make sure we return the result out of the procedure or the allocated data
will no longer be referenced and that allocated memory will sit around for the program’s lifetime unused: a memory leak. This is also obviously the case with heap-allocated basic types.
Ultimately, everything is passed in and out by copy, but with heap-allocated data you either get a pointer to that data you can copy out, or the struct contains a pointer to the data anyway, so it basically has reference semantics without the need to expressly return a pointer.
When to allocate/deallocate memory?
Ultimately all memory needs to be allocated and deallocated. The difference is that we can consider the stack an automatic memory manager in that we don’t have to explicitly deallocate if we don’t care about the data outside of a procedure’s scope. For everything else, there’s the heap.
We should allocate memory for data on the heap when:
- We need it live longer than the current stack frame it was allocated in
- We need to modify the data directly in memory rather than a copy of the data when passing it into procedures
We should deallocate memory for data on the heap:
- When we no longer have use for it at that point in our program
- To clean up data types that implicitly allocate memory (
map
,dynamic
and potentially other package’s data/procs)
Some data may just want to last the entire lifetime of the program, in which case we can just let the OS clean up on program exit, but for shorter lived objects, we’ll need to allocate then free at some point.
What about structs?
Structs are copied like everything else, but obviously if the struct contains a field that is a dynamic
or map
or pointer to some heap memory, then if that struct gets popped off the stack, the memory created on heap will still remain. So we need to make sure we free the relevant fields - this seems like a non-trivial problem because you can’t pass a whole struct to something like free()
QUESTION: Look into how we would deallocate a struct full of pointers cleanly? Is there a well-known pattern? Do we build a custom destructor for it?