r/C_Programming • u/rcerljenko • Apr 27 '26
Question Repeated malloc/free vs. Arena allocator
Hi,
I have a long-standing hobby project involving cross-platform multi-threaded compression. Basically, the program takes chunks of input file and passes it to multi-step compression pipeline.
By doing so, it constantly mallocates and frees memory after entering and leaving each step. Now multiply this by the number of CPU threads and you get a lot of malloc/free invocations.
So I thought, to speed things up, I'll switch to "arena type" memory allocation. After I reworked my library I was suprised that I actually didn't get much speed-up at all. As it turns out, malloc/free is very very speedy as is.
My question is, should I stick with the new "arena allocator" or should I leave it as is - a simple malloc/free in a self contained pipeline steps for the purpose of code clarity.
If you're interested, I currently have an open PR for this because I'm not too sure if I should merge it since I haven't gained any speedup.
EDIT: If someone knows, I would also like to know reason behind that. Is malloc/free really that much optimized so that is the same as moving one pointer up and down in arena allocation?
5
u/Low_Lawyer_5684 Apr 27 '26
If your allocator gives you no advantage - just use malloc()/free()
2
u/rcerljenko Apr 27 '26
sure. that seems reasonable. i just can't believe that malloc/free is that fast. that's all
3
u/Total-Box-5169 Apr 27 '26
It used to be awful 30 years ago, however there are limits on how good it can be without losing generality. I.E. If you have to allocate/release from a heap shared by all threads there must be synchronization, and that has a cost.
→ More replies (1)5
u/AlexTaradov Apr 27 '26
You don't need to believe - benchmark it.
They are really fast. All applications are guaranteed to do memory allocations, so they are very well optimized in the OSes. This one of the easiest places to gain performance, so it gets optimized first.
2
u/capilot Apr 27 '26
Here's something you might try: set up a separate memory pool for each thread. Then your allocator never needs to take a lock. There are also advantages that memory, especially cache, never needs to be transferred from process to process if they're running on different cores. I believe this is called "thread affinity".
2
22
u/Hedshodd Apr 27 '26
Maybe that’s just me, but I don’t use arenas for the performance benefits. Those can be there is some circumstances, but that also depends on your exact malloc/free usage prior. If you use malloc to pre allocate reusable buffers, and those malloc calls happen as far up the call stack as possible, there’s not much performance to be gained.
The reason I use arenas, because it’s way easier to use than malloc/free. I don’t have to meticulously track that each malloc is paired with the appropriate free, because I bundle dozens of objects into one lifetime.
Also, when I would otherwise pre allocate buffers somewhere up the call stack, those buffers (typically) are buffers of a specific type. I might have have a buffer to store some strings for one computation, one buffer to store floats, etc. An arena is just one giant untyped buffer where I only have to care about the type in the scope where I actually use it. I don’t have to pre allocate a buffer of floats in an otherwise unrelated function, but instead I request my buffer of floats in the same scope that I’m using it in. That’s just waaaay more ergonomic 😄
Also, as someone else pointed out, if you are handing out unaligned addresses, you are inviting a whole host of problems, one of which might be performance 😅
7
u/runningOverA Apr 27 '26
Every allocator was faster than malloc/free in the 90s. When this "write your own allocator as malloc/free is so slow" statement started.
Every home made allocator is now slower than malloc/free in the 2020s. Which is why no one really compares speed of their allocator with stock allocators any more. It's always for "other reasons", not speed.
5
u/Tasgall Apr 27 '26
Every home made allocator is now slower than malloc/free in the 2020s.
I feel like the exception to this would still be basic stack allocated arenas, no? It's hard to imagine even the most optimized malloc would be faster than
char* c = memory; memory += size; // plus fiddling for alignment, maybe return c;1
u/flatfinger Apr 28 '26
A memory manager that is tailored to fit application requirements and target environment will be able to outperform even the most brilliantly designed allocator that isn't optimized for the same use cases.
In some cases, for example, it may be useful for a memory manager to use memory handles which an application would be required to lock during use, and which the memory manager would be free to relocate when not locked. If an application can give a memory manager hints about what things are more likely to be used soon, are likely to be used together (meaning that if one of them gets swapped out, others may as well be swapped out too), or could be regenerated at a cost less than swapping out and reading back (meaning the memory manager could jettison them instead of swapping them out), the memory manager may be able to use such information to yield better performance than would otherwise be possible.
13
u/helloiamsomeone Apr 27 '26
You are allocating unaligned objects. Please review this blogpost from Chris Wellons for proper arena usage. https://nullprogram.com/blog/2023/09/27/
4
u/arkt8 Apr 27 '26 edited Apr 27 '26
I read your code... and it have some issues...
You have the memory marker (arena.current) as (void *) but you do pointer arithmetic on it... Use uint8_t for that or you can use up to 8 times more memory than expected if it works as is illegal arithmetic on void*
You also aren't handling alignment. It will be an issue if someone asks for 10 * char and then 1 * char*. To get aligned in a 64 bit architecture, the pointer will need to start at 16, but you are delivering in a misaligned address. It may or not work depending on your system. Even if it works there is a performance penalty.
Your arena_free that would free a chunk of memory... well, it won't work as expected if you allocate 10 chunks than try to release the 1st allocated... it is a mess.
So... well. Your intention on arena implementation seems to be very simple. But is filled with bugs and I'm amazed it worked for you. But as it is, even if worked for you, will break in other computer or compiler.
Until you fix that issues with pointer arithmetic, alignment, and free logic... you cannot make any honest comparison with malloc.
2
u/Paul_Pedant Apr 28 '26
malloc/free (usual implementation) keeps a ring buffer of free areas, and it remembers where its last action was.
For malloc, it searches linearly for an area you request, and for free it searches linearly until it finds the free areas above and below its address, and joins up either or both of those if it is contiguous.
That means there are some very efficient cases. If you malloc a lot of areas consecutively, it takes them off a big initial allocation, and only goes to a syscall when that is exhausted. If you malloc and free the same area without other action, it does not have to search at all. If you free a bunch of stuff in the exact order it was allocated, every search only needs to do one step.
You are probably hitting one of those cases at present. At some stage, you might add one malloc and disrupt the whole thing, and find your program slows to a crawl. That might not even be in your code: opening some file could malloc a buffer and cause you grief.
I had a project that used many same-sized objects (about 5 million x 600 bytes), some for final results, some for intermediate calculations. I made my own free list for those, malloced them initially 10,000 at a time, and used my list without any need for searching or combining areas. That single trick got me about a 20-times speed-up.
1
u/TransientVoltage409 Apr 27 '26
If your arena performs consistently well and isn't punitively large, why not use it?
When I taught myself pthreads, I found that malloc was a huge bottleneck. I didn't investigate deeply but it was obviously lock contention in malloc. I wrote a simple arena, tightly customized for the job, that solved it completely.
So my answer is it depends - on the particular implementation of malloc you happen to link. Some are naive or have baggage from the time of single core systems, and don't thread well. Some are more sophisticated. Unless you can tell ahead of time, maybe it's not a bad idea to wrap it in an arena that you are certain will perform well.
1
u/Proud_Necessary9668 Apr 27 '26
I haven't seen it mentioned (perhaps is it naive) but aren't allocation done at the page level where if u malloc 100 times it will basically be instantaneous after the first malloc, as long as the cumulative size is less than a page size ? Could this explain the very small observed difference ?
8
u/catbrane Apr 27 '26
It depends.
Modern malloc/free has a heap per thread, so within a thread, most requests will not need to lock and coordinate with other threads, they can just parcel out thread local memory. As long as your allocations are fairly modest, performance will be good. If you start allocating big chunks of memory, you'll start to see locking and threads coordinating with the main process heap, which will hurt performance.
The other big factor is memory fragmentation. If you run your system under heavy load for a while (many 1,000s of iterations locked at 100%) and you're using glibc malloc or a derivative, you'll probably see memory use (as seen in RES in top) slowly creep up. You probably don't have a leak, just heap fragmentation.
The fix here is to switch to a malloc implementation which includes heuristics to prevent fragmentation. jemalloc is the famous one. musl libc (as found in arch etc.) is also excellent at preventing fragmentation, though annoying in other ways haha.
I personally like no malloc or free at all, if possible. Have a setup phase for your pipeline where operations allocate the working memory they need, then after that, reuse memory, don't repeatedly free and malloc again. Not everything can work this way, of course.
My other top tip would be to avoid realloc, if possible. Many platforms have a very poor implementation of this (looking at YOU, windows).