Should we regularly free context.temp_allocator, even if unused?

I noticed two things about context.temp_allocator

  • Core lib procs implicitly allocate in it.
  • It’s by default an Arena allocator.

Does this mean that the user code has to regularly clean it up, even if it is never used in user code? Otherwise, it’s a type of leak.

The idea of the temp allocator is that you release everything allocated with it at the end of the update loop, request handler, etc… using the free_all proc

So, is that a “yes”?

Yes

But it’s a lot simpler than having to track every temp allocation. It’s just a simple arena allocator, so free_all just sets the pointer to the start of the arena and it’s up to you to be sure that you aren’t keeping any references to data in the arena across frame/request/whatever boundaries

1 Like

I think the answer is actually “No”, not “Yes”.

(The original question was basically “Do you have to call free_all on the temp allocator manually if the user code calls a procedure from the core library that uses the temp allocator?”)

Glancing at the Odin source code, I don’t see any core lib procedures that use context.temp_allocator to allocate something that is returned to the user. Instead, what I see are either 1) the temp allocator is used to allocate something that is then freed before the proc returns or 2) a struct is returned that has a pointer to a temp allocator, but nothing was actually allocated using it (yet). Both of these cases don’t require the user to call free_all on the temp allocator, because there is nothing to free when the procedure returns to user code.

Using the temp allocator to allocate something and return it doesn’t make sense. It’s like allocating something on the Stack and returning a pointer to it when returning from a procedure (which is bad). If the core libs returned stuff allocated with the temp allocator, it would make the temp allocator unusable in other parts of the code because then that other code couldn’t call free_all at the end of their temp allocator usage without invalidating what you allocated.

My understanding is that the temp allocator is effectively used like the Stack - anything allocated on it should be freed before returning from a given procedure. And anything that you want to allocate and return should use context.allocator.

1 Like

the temp allocator is used to allocate something that is then freed before the proc returns

Mmm this doesn’t sound right. the default temp allocator is an arena allocator. You can’t free individual allocations there.

… Unless you use Arena_Temp. You can free a region of memory at the very end of all allocated memory using that, so the core library could safely free the memory it used.

Well I’d have to look if they actually do that.

  1. a struct is returned that has a pointer to a temp allocator, but nothing was actually allocated using it

Right, i’m pretty sure they would never return something that was internally allocated on the temp allocator, unless you passed that allocator yourself.

1 Like

(UPDATE) I think you are right, so maybe your concern about an implicitly growing temp arena is valid.

I guess this makes sense if the procedure is marketed as using the temp allocator (e.g. tprintf), but then it is understood that the user needs to at some point free the temp allocator.

What doesn’t make sense to me yet is this, from core/container/queue/queue.odin:

shrink :: proc(q: ^$Q/Queue($T), temp_allocator := context.temp_allocator, loc := #caller_location) {
	if q.data.allocator.procedure == runtime.nil_allocator_proc {
		return
	}

	if q.len > 0 && q.offset > 0 {
		// Make the array contiguous again.
		buffer := make([]T, q.len, temp_allocator)
		defer delete(buffer, temp_allocator)

		right := uint(builtin.len(q.data)) - q.offset
		copy(buffer[:],      q.data[q.offset:])
		copy(buffer[right:], q.data[:q.offset])

		copy(q.data[:], buffer[:])

		q.offset = 0
	}

	builtin.shrink(&q.data, q.len, loc)
}

So, if the temp allocator is a basic bump arena allocator, then delete() here wouldn’t do anything and the temp allocator would grow and need to be freed by the user.

But, if the temp allocator is some kind of arena allocator with the ability to only free the last thing it allocated, then this wouldn’t leak anything with the defer. But that also assumes that nothing uses the temp allocator in between, and if it did, things would break.

So I’m still confused. I need to play around with it more.

Perhaps the confusion is related to Temporary allocators and the core library.

Useful info here from a real world usage case: temporary-allocator-your-first-arena

The notion of a memory leak is different when comparing an explicit heap allocator (like context.allocator) to an arena allocator. Furthermore, the context.temp_allocator is a special arena allocator available to all scopes as long as they have access to context through the default “odin” calling convention or other means.

Let’s compare…

For an explicit heap allocator (like context.allocator), memory leaks (and bad frees) are more easily defined. If memory is allocated in a particular scope, and ownership is not passed on to another scope, the opportunity to delete that memory is gone, therefore a leak has no doubt occurred, and is track-able. One can safely assume a delete was forgotten or left out somewhere. Also, if that same memory has been deleted more than once, no doubt a bad free has occurred. There is very little ambiguity in this area.

For an arena allocator, the allocation of memory is more tied to a “lifetime” of your choosing. Multiple categories of “lifetime” may exist for an application, so multiple arenas may be required to fulfill each category. What is a leak when using an arena? That’s only definable by the developer who decides on what a “lifetime” means. There’s no way for Odin or a tracking allocator to know what the intended lifetime should be. To those entities the default lifetime is till program exit, which frees all memory at that time anyway. It is still possible to have a “classic” memory leak when using arenas if an arena is initialized in one scope, but ownership of that arena variable is not passed on to another scope, then the opportunity to free that memory is lost, and therefore a leak has occurred (maybe). Who’s to say if that was intended to satisfy a specific lifetime requirement. The ambiguity is left to the developer to resolve through logic. The added benefits are: 1. Clear lifetimes must (at least should) be defined, thus categorical analysis of memory usage by a developer is possible by tracing arenas. 2. There is no need to delete individual allocations, which usually require careful attention to how they were allocated. 3. There is no such thing as a bad free, since free_all() will free memory only if there is any to free.

How is context.temp_allocator special? It simply is a globally available arena independent of scope depending only on availability of context through the before mentioned default “odin” calling convention or other means. It’s use case is clear; if you are only allocating a little memory here and there for a short lifetime before program exit, and/or you don’t want/need to define your own arena, and/or don’t want/need to pass an allocator parameter around, then there is nothing wrong (leaks or otherwise) about using context.temp_allocator and not freeing it. However, it is a growing arena, so if it is used prolifically in many many loops, then you may need to make decisions on what “lifetime” that temporary data should have and free_all() where appropriate. Maybe consider defining your own arena instead in those cases.

So the ultimate answer is, no, you do not need to regularly free context.temp_allocator, unless you are using it in such a way that it may grow exponentially out of control. And if that’s the case, it may be best to define your own arena or use context.allocator to make tracking leaks more explicit. The context.temp_allocator will initialize a capacity of 4194304 Bytes (4.00 MBs) on the first time use. Most of the time, a program rarely ever uses more than a few hundred KBs of that, but if you find memory usage climbing closer to that 4 MBs, it may be time to rethink memory strategies. I believe the next capacity jump is a power of 2, so if needed, the next capacity level will be 8MBs, then 16MBs, and so on. Personally, if I get above 2-3 MB mark, I start to think about if I still want to use it or switch to a different approach. If I’m allocating hundreds of MBs or several GBs, I definitely will not use context.temp_allocator. I will either assign a specific arena for that large chunk, or use context.allocator so that I have a clearly defined handle to that memory.

You can check your usage like this (my tracker also prints these values :slight_smile: ):

used := (^runtime.Default_Temp_Allocator)(context.temp_allocator.data).arena.total_used
cap  := (^runtime.Default_Temp_Allocator)(context.temp_allocator.data).arena.total_capacity

I see fmt mentioned often when discussing context.temp_allocator. I’d like to point out that even though many of the commonly used fmt.print procedures do use context.temp_allocator, there is also:

  • fmt.aprint series of procedures available if one wants to format a string with a specific allocator that has a different lifetime than context.temp_allocator.
  • fmt.bprint series if one wants to use a [ ]byte buffer.
  • fmt.sbprint series if one wants to use a string builder.
1 Like

That makes a lot of sense, thanks. So if I were to summarize, a library procedure that uses context.temp_allocator isn’t considered as leaking anything, but it does make the temp allocator arena grow, which will become a problem if you endlessly call it in a loop without ever calling free_all() periodically in user code somewhere (since the core library will never call free_all()).

So basically, if you don’t plan on using and freeing the temp allocator in your code, then avoid using any procedures that use the temp allocator to guarantee no unbounded growth (maybe set temp allocator to the panic allocator to guarantee this?).

Basically, yes. Though I’d change does to might.

Not necessarily. You’ll notice that core library procedures that use ‘context.temp_allocator’ do so for data that has an obvious short lifetime and also (mostly) for data measuring in Bytes or KBs. Those procedures are not likely going to break the memory bank (to coin a phrase). If you are using a 3rd party library, you may then want to look through the code and think about if memory was handled properly. The answer may be no, but not necessarily. The most likely case for unbounded growth is if in your own code, many allocations are occurring in a loop. In that case, there is an obvious need to manage memory regardless of the allocator used. Using ‘context.temp_allocator’ is likely the least of your problems, but an indicator for closer examination.

Not sure I could ever recommend this. Seems drastic to me, though if it makes you feel better, sure. The result will be to cut yourself off from some very useful and harmless procedures/libraries. Like my tracker. It uses ‘context.temp_allocator’ after printing the metrics for it, so as to not muddy the water. It does so, because the data only measures in Bytes, and is done right before printing other data (very short lifetime), and in most cases (depending on how it’s used), the last thing done before program exit (also very short lifetime).

I think it’s better to arm yourself with knowledge and experience of memory management in general. Then there will be no memory monster lurking (leaking? :wink: ) around every corner.

1 Like

GingerBill discusses memory allocations and arenas in this podcast.

1 Like

More very good reading can be found in Odin’s core files:

Odin\core\mem\doc.odin

Odin\core\mem\virtual\doc.odin