Feedback on Custom Allocator Patterns

I’ve been thinking about ways to handle memory in Odin. I used to use both allocators in context a lot, but after seeing Bill say multiple times that I’m using it outside its intended use, I started trying other ways.

I also read in some places that I should be using virtual memory allocators, if my platform supports it. So I’m trying to do that.

I was wondering if doing something like this is acceptable for a “temp” allocator that clears every frame, or for other uses like “all memory associated with a game scene”:

package allocators

import "core:fmt"
import "core:mem/virtual"
import "core:mem"
import "base:runtime"

arena : virtual.Arena
arena_alloc : mem.Allocator

print_arena :: proc(desc: string) {
	fmt.printf(desc)
	fmt.printfln(" - us: %v, reserved: %v", arena.total_used, arena.total_reserved)
}

main :: proc() {
	alloc_err := virtual.arena_init_growing(&arena, 1000)
	assert(alloc_err == .None)
	
	arena_alloc = virtual.arena_allocator(&arena)
	print_arena("init")
	
	data := make([]byte, 100, arena_alloc)
	print_arena("after a make")
	do_thing()
	
	print_arena("after proc")
}

do_thing :: proc() {
	TEMP_GUARD(&arena)
	// equal to:
	// temp := virtual.arena_temp_begin(&arena)
	// defer virtual.arena_temp_end(temp)
	
	print_arena("in proc - before make")
	
	data := make([]byte, 23000, arena_alloc)
	
	print_arena("in proc - after make")
}

// Convenience function for clearing used memory in scope
@(deferred_out=virtual.arena_temp_end)
TEMP_GUARD :: #force_inline proc(arena: ^virtual.Arena, loc := #caller_location) -> (virtual.Arena_Temp, runtime.Source_Code_Location) {
	return virtual.arena_temp_begin(arena, loc), loc
}

// Output:
/*
init - us: 0, reserved: 4096
after a make - us: 100, reserved: 4096
in proc - before make - us: 100, reserved: 4096
in proc - after make - us: 23100, reserved: 28672
after proc - us: 100, reserved: 4096
*/

For long lasting allocations that should live throughout the whole program, I was thinking of using the Scratch allocator wrapped in a Tracking allocator.

Some questions:

1: Does the code make sense?

2: I’ve heard that we should probably allocate memory in allocators directly, instead of using them through the runtime.Allocator interface. Isn’t the unified interface convenient, because of make() and others?

3: Why is there only the Arena type allocator in mem/virtual?

References: Temporary allocators and the core library

  1. Yes and No. Yes I see how it works and No, I don’t understand why one would do that as the main approach to memory management. Though, I’m always up for learning something new.
  2. I think this might be referring to using the pre-defined context.allocator rather than defining your own (though you can if you wish to manage that), though I could be wrong on that.
  3. I could swear there was mention of that fact in Karl’s book, but I can’t seem to find it at the moment.

Personnally, I would not try to manage a single arena and slice it up in that way. Things could be difficult to debug, etc. Personnally, I’d make an arena for each category/lifetime of dynamic memory you need. So, create a new arena when game level loads for level data only, then destory it when it ends. Have a frame loop arena for drawing in a loop, and clear after each frame. Have an arena for player data that persists in a logical manner. etc, etc.

Oh I absolutely agree. I am also using separate arenas for different things. Things will get clearer after my current project develops more. But I’ve done the arena pattern in the past and it works very well. By that I mean: storing an arena reference in a struct and hold all of that struct’s memory in that arena, then free it when that struct is no longer of use.

I’m looking forward to using other types of allocators and really learning when I should use which.

I’ve come to internalize/personify the decision of what allocators to use. I think in terms of “mine” (me writing the procedure) and “his” (me in the 3rd person using the procedure).

So if I have a procedure that allows an allocator as a parameter, because it returns allocated memory, I’ll only use the supplied allocator parameter for the returned data, but use what ever allocator is needed temporarily in that procedure, cleanup, then allocate the return.

So for an exec procedure that returns allocated stdout (which is only for executing external programs that return bytes i can use and then quits on it’s own) I do this:

@(require_results)
exec :: proc(command: []string, allocator := context.allocator) -> []byte {
	desc := os.Process_Desc {command = command}

	// "mine" explicit using context.allocator and not allocator parameter
	state, stdout, stderr, error := os.process_exec(desc, context.allocator)
	defer delete(stdout)
	defer delete(stderr)
	stdout = bytes.trim_right(stdout, {'\n'})
	stderr = bytes.trim_right(stderr, {'\n'})
	if len(stderr) != 0 {
		fmt.printfln("%s", stderr)
	}
	if !state.success {
		fmt.printfln("%s: %s", desc.command[0], os.error_string(error))
	}

	// "his" returned allocation using allocator parameter, which can be any allocator
	return bytes.clone(stdout, allocator)
}

Then I can choose to use a runtime allocator, or an arena when I use the procedure and not care about what happens internally, just deal with the return.

Usage for edification:

main :: proc() {

{
	result := exec({"ls", "-l"}, context.allocator)
	defer delete(result)
	fmt.printfln("%s", result)
}
	// or
{	
	virt: virtual.Arena
	arena := virtual.arena_allocator(&virt)
	defer virtual.arena_destroy(&virt)
	result := exec({"ls", "-l"}, arena)
	fmt.printfln("%s", result)
}

}
2 Likes