Allocators and what I learned

@author bonmas14

Allocators and what I learned

Last year I finished my graduation project and started making games and my own standard library. I was learning a lot about programming and was shifting to program from C# to C++ and then finally to C. And one of the things that I found useful from other people who make actual software, was an idea of having general interface for managing memory: just a struct that contains two pointers, to allocator procedure and to the allocator data.

But as most of people going from OOP world, I had a lot wrong ideas which proved again, that overgeneralization of everything is always bad and how generic allocator struct is not for everything. I will share some of the things that I found not to be useful.

Premature optimizations...

I don't know why I am always stumbling over this, it should be simple to follow: just don't optimize before you know what should be optimized. And then I found myself making a quadtree for no reason, but it always more than that, stuff like picking pointers over passing struct is premature optimization and that is exactly what I had in my graduation project.

I decided that I want to be efficient and not pass allocator struct by value: "because its 16 bytes, that's no good, it will copy over, everything will be slow." — it won't. What will happen in practice is that you now need to preserve the allocator from stack and have value of structure to be stored somewhere too and not on the stack, because then references will break and note that: it literally stores pointers, so doing this is useless — that's wrong level of indirection and it only confuses you in the process.

I want to say that any analysis of speed based on just looks of the code is not any better than O notation, it is very vague. And just thinking that passing value by pointer is always better, is not a good idea. One example of when you actually don't need this is simple "costructor":

void sometype_init(Sometype *v) {
    v->some_a = 100;
    other_init(&v->some_other); 
} 

I don't like when I see people do this, because it doesn't make that much of sense. Calling conventions define that if struct or value is over machine word size (or double in case of SystemV), it will do this job for you:

Sometype sometype_init(void) {
    Sometype v = { 0 };
    v.some_a = 100;
    v.some_other = other_init(); 
    return v;
}

Here, return value is getting passed by a pointer as a secret input argument anyway, so why not use what we got? Also it will make caller side less confusing, and you can see how I applied this with other_init(). And not only that, it enforced you to write in a better way, where errors are part of expected behaviour of the program. Ryan Fleury has great post about this.

Allocators are interface

This is main point why everything didn't work for me when I implemented it and used for about a year. For some reason I confused the generic allocator structure with type that actually contains information, like entity in a game (Allocator still contains data, but it is literally struct of pointers, so we can call it a vtable). So I designed entire system over the idea that we always pass around this type. And seems that this confusion is not just me being dumb, GingerBill in his post about contexts in Odin talks that simillar confusion made people not use contexts, which actually made them work:

"The main purpose of the implicit context system is for the ability to intercept third-party code and libraries and modify their functionality." "Ironically, context works because people misunderstand it, and thus generally leave it alone." gingerBill — context—Odin’s Most Misunderstood Feature

As with contexts in Odin. You shouldn't use this generic interface in your codebase. This is what you use to redefine the behaviour of a code you don't own. But what I did in my library was the opposite of that and it became annoying to deal with later.

In this library (which I call ungrateful) I have the an allocator structure that was defined the classical way, pointer for data and pointer for procedure. And as I said problem was in a way how I created instances of allocators:

Allocator un_alloc_arena_create(u64 init_size);

Descriptive, does what it says, but it arena gets downcasted to this allocator type and suddenly all of the information about what type it was evaporated. Now I just can't do things that I would (and wanted) to do with this Arena, and it gets even better.

void un_alloc_arena_destroy(Allocator *alloc);

Can somebody know what type of allocator could be passed as input argument? How I can be sure that I passed the Arena and not the VirtualAlloc/mmap/malloc wrapper? Yes, I don't know, and never will be able to if I keep programming like this.

The only way I kept using this library for a year was my temporary allocator, And it actually shows why I didn't get why this was wrong that fast. 90% of time all you need is a temporary allocator, and I just defined all of the useful stuff for temporary allocator and kept using it always. But when I tried to actually use the other parts I was getting a lot of unnecessary friction that slowed me down.


@date 8th July 2026