Arena Allocators in C

Lobsters Hottest News

Summary

一篇讲解 C 语言中 Arena 分配器的技术指南,介绍如何通过批量分配内存块来替代传统的 malloc/free 模式,从而提升性能并减少内存泄漏。

<p><a href="https://lobste.rs/s/zziknb/arena_allocators_c">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 10/11/26, 11:39 PM

# Arena Allocators in C Source: [https://eliasebner.com/blog/guides/arena-allocators-in-c/](https://eliasebner.com/blog/guides/arena-allocators-in-c/) --- As we all know, C is a very low level language\. It allows you \(and kind of forces you\) to manage the memory you use by yourself\. This is one of the \(probably many\) reasons that people are turn to other languages when it comes to their daily programming\. In this article, I will show you a paradigm for managing memory which is not only very performant, but it’s also extremely versatile\. It makes the task significantly easier and decrease the chance of memory leaks by a large margin\. Maybe you already know about them, maybe not, in any case: I’m talking about arena allocators\. I will get to the implementation, but before I do that, here is a brief introduction to the concept of an arena allocator\. ## What Are Arena Allocators? The way people usually go about managing memory is that they allocate a small amount of memory every time they need it from the heap with`malloc`\- just what they need\. If they find out later that they need more than what was originally planned, they use`realloc`to increase the size of their memory allocation\. Once they are done with using that memory, they call`free`to tell the operating system that this memory is no longer needed and it can be employed for other tasks\. This is great, but it has a few issues: **1\.** The programmer must remember to free the memory once he is done using it\. This is especially dangerous if the allocation happens in a loop \- like in games, for example \- since in that case the operating system keeps allocating more and more memory, but that memory is never freed\. Memory, as a resource, is not infinite\. At some point, your computer will run out, and your program will be terminated by the operating system \(i\.e\. it will crash\)\. **2\.** Allocating and freeing many small chunks of memory is*slow*\. **3\.** If you have a function like: ``` i32 *create_array(usize len) { i32 *elements = (i32 *)malloc(sizeof(int)); return elements; } ``` Now, tell me: how does the programmer calling the function know that this function performs a memory allocation? In this case, one could figure it out by looking at the name of the function and thinking about what the function could possibly be doing, but it’s not always so trivial\. Remember, this is important, since the programmer needs to know when to free the memory\. The programmer needs to be made aware of the fact that it is his responsibility to free the memory\. There are probably more issues that I have not thought about, but these are some of the main ones I could think about on the spot\. **The Solution\.** Enter*the arena allocator*\. Most lifetimes \(i\.e\. how long a certain region of memory is used before it is freed\) can be grouped into one of a handful of categories: 1. The memory lives for the duration of the entire program 2. The memory lives until the completion of some tasks \(i\.e\. a frame in a game or the handling of an HTTP request\) 3. The memory lives for the scope of a function This is usually enough grouping, but depending on what you consider a group, there may be more\. In any case, we can just allocate a large chunk that we will use to work with things that have a similar lifetime \(i\.e\. things that can be freed together\)\. We then have a large amount of memory that can be used\. Whenever we use some of that memory to do something, we advance a pointer to remember up to which point the memory has been used and where the free memory begins\. This will become clearer once you see the implementation\. When we don’t need any of the data that lives inside that large chunk of memory anymore, we can simply free it all at once \(or \- if we want to reuse that same memory \- reinitialize the memory arena to its initial state and begin allocating from the start again\)\. This saves individial memory allocations \(which remember, are slow\), and makes us have to worry less about managing memory since we only have to worry about a couple of different “chunks” of memory instead of small pieces used for individual arrays\. ## The Implementation ### Clarification Before we start, I almost always include this file at the top of my code when I use C\. It simply`typedef`s some built\-in types and has some useful, commonly used utilities\. I include`\.c`files directly, since I use unity builds for faster compilation\. You can easily write the header files for this if you prefer that\. ``` // types.c #ifndef TYPES_C #define TYPES_C #include <stddef.h> #include <stdint.h> typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; typedef uint64_t u64; typedef int8_t i8; typedef int16_t i16; typedef int32_t i32; typedef int64_t i64; typedef float f32; typedef double f64; typedef size_t usize; #define KiB(x) x * 1024 #define MiB(x) KiB(x) * 1024 #define GiB(x) MiB(x) * 1024 #define MIN(a, b) a < b ? a : b #define MAX(a, b) a > b ? a : b #endif // TYPES_C ``` ### `arena\.c` Here is the actual arena implementation\. We start with the include guard \(again, because of the unity build\), and include some`libc`headers we will use: ``` #ifndef ARENA_C #define ARENA_C #include "types.c" #include <stdbool.h> #include <stdlib.h> #include <string.h> // code goes here #endif // ARENA_C ``` Here is the arena struct definition: ``` struct arena { void *data; usize size; usize used; }; ``` It keeps track of how much memory was used\. This could also be achieved by using a pointer that points to the start of the region in memory that is still unused\. The`size`field is needed to understand if we are overflowing the arena when allocating \(that could very well happen if you are using too much memory\)\. And`data`simply points to the start of the arena\. So far so good, but now we need a way to create a new arena\. Here it is: ``` struct arena_create_out { struct arena arena; bool success; }; [[nodiscard]] struct arena_create_out arena_create(usize size) { struct arena_create_out out = {0}; out.arena.data = malloc(size); if (out.arena.data == NULL) return out; out.arena.size = size; out.arena.used = 0; out.success = true; return out; } ``` When returning multiple values, instead of using output parameters, which I find confusing and it makes the API unintuitive in my opinion, I much prefer to return structs\. We then need a way to allocate some memory in this arena\. Before this, define this macro \(I have it at the top of the file, but I am introducing it now since this is when it becomes relevant\): ``` #define ARENA_ALIGN 8 ``` This will be used to align the data\. For some reason, our computers are much better at dealing with data that is aligned at 8 bytes\. This means that the start of each new allocation has to be at a multiple of 8 bytes\. `malloc`handles this for us, but since we are not allocating with`malloc`now, but simply reserving some space in an already allocated chunk of memory, we have to align the data ourselves to make for faster accessing\. Here is the allocation function: ``` struct arena_alloc_out { void *ptr; bool success; }; [[nodiscard]] struct arena_alloc_out arena_alloc(struct arena *arena, usize size) { struct arena_alloc_out out = {0}; usize alignment = (ARENA_ALIGN - (arena->used % ARENA_ALIGN)) % ARENA_ALIGN; out.ptr = arena->data + arena->used + alignment; if (out.ptr + size > arena->data + arena->size) return out; arena->used += size + alignment; out.success = true; return out; } ``` Here, we understand how much “padding” we need to add \(the`alignment`variable\) at the end of our allocation such that the next one will be aligned to 8 bytes\. I use the pattern of having a`success`field in the return type of the function, and on error I simply return early \(since it is initialized as all\-zeroes with`= \{0\}`,`success`will be`false`\)\. If you don’t like this approach, it should be fairly trivial to migrate the API to a different convention\. Most of the work is done, I created a simple wrapper to change the size of an allocation\. It simply allocates at the end of the arena with the new size and copies the data over\. The old data is left untouched as it will be freed once the arena is freed \(remember, we cannot free individual allocations, it’s either the whole arena or nothing\)\. You don’t*really*need it, but I found that it made my code cleaner, so here it is: ``` struct arena_realloc_out { void *ptr; bool success; }; [[nodiscard]] struct arena_realloc_out arena_realloc(struct arena *arena, usize new_size, void *src, usize old_size) { struct arena_realloc_out out = {0}; struct arena_alloc_out alloc_out = arena_alloc(arena, new_size); if (!alloc_out.success) return out; out.ptr = memcpy(alloc_out.ptr, src, MIN(old_size, new_size)); out.success = true; return out; } ``` If the new size is smaller than the new size, then a part of the data is cut off at the end\. Also, internally`realloc`knows the size of each allocation associated with each pointer, so you do not need to pass the old size, only the new size\. Since our arena implementation does not keep track of the size of allocations in any way, we will have to pass the old size to the function separately\. The user of the arena is expected to know the size of each allocation\. This could also be implemented, but I would like to keep it simple\. The next two functions are small wrappers, they’re only there for convenience but are not essential: ``` void arena_clear(struct arena *arena) { arena->used = 0; } void arena_free(struct arena *arena) { free(arena->data); } ``` This is it for my implementation\. ## Example Usage I have a simple string library that I have implemented for my own use cases\. It has two concepts: a string view and an owned string\. An owned string is NUL\-terminated and`owns`its data \(i\.e\. it has to be allocated and freed\)\. That also means that one can modify an owned string directly\. A string view only*views*the data, so you cannot modify it directly, since there may exist multiple string views viewing the same data and modifying it would lead to unexpected changes to the other strings\. On the other hand, only one owned string shall exists for any given region of data\. What is important is that I need a way of converting string views to owned strings\. To do this, we have to create a new owned string \(which means allocating the memory for it\), and copy the data over\. Again, this is to make sure that changes to this owned string do not interfere with other potential string views viewing it\. We also add a NUL\-terminator at the end of the owned string \(to make it compatible with other legacy C APIs\)\. To do this, we pass an arena allocator to the function and allocate to that arena: ``` struct str_view { const u8 *data; usize len; }; struct str_owned { u8 *data; usize len; }; struct str_view_to_str_owned_out { struct str_owned str; bool success; }; [[nodiscard]] struct str_view_to_str_owned_out str_view_to_str_owned( struct arena *arena, struct str_view s ) { struct str_view_to_str_owned_out out = {0}; struct arena_alloc_out alloc_out = arena_alloc(arena, s.len + 1); if (!alloc_out.success) return out; out.str.data = alloc_out.ptr; memcpy(out.str.data, s.data, s.len); out.str.len = s.len; out.str.data[out.str.len] = 0; out.success = true; return out; } ``` This does two things: 1. The caller immediately knows that this function allocates memory \(since its first argument is an arena pointer\) 2. The caller can decide where \(i\.e\. with which lifetime\) to allocate the memory\. Regarding point no\. 2, since the caller decides the lifetime by passing the relevant arena, they do not have to worry about freeing the memory directly, the memory is simply freed once the lifetime is over and the arena is discarded \(or cleared\)\. This could be at the end of the program \(in which case freeing the memory would be unnecessary, since the operating system handles it for us\), or at the end of the function, or whenever they are sure that the memory is not needed anymore\. ## Conclusion I recently started including arenas in my programming habits and I must say that I have been really satisfied\. I have migrated my string library to use arenas and I think you could benefit a lot from using arenas as well\. Sometimes the gains in developer experience are larger than in other cases, but I would say that having this tool under your belt as a programmer is definitely a net benefit\. I don’t claim to be an expert and this implementation is just something that I have built myself for personal use\. It may not be perfect, but it should serve as a nice introduction to get you started\. Thanks for reading and I hope you found this useful\! As always, for any questions, feedback, or insights, you can reach me at my email: [info@eliasebner\.com](mailto:[email protected])

Similar Articles

ArenaAllocators don't play nicely with ArrayLists

Lobsters Hottest

一篇技术博客探讨 Zig 中 ArenaAllocator 与 ArrayList 增长机制的不兼容问题:ArrayList 的「分配-复制-释放」模式使旧内存几乎永远无法归还给 Arena,最坏情况下可能导致约 3 倍内存占用。作者建议通过预分配容量(initCapacity/ensureTotalCapacityPrecise)和避免交错分配来缓解该问题。

Pony's Arena Allocator

Lobsters Hottest

The article describes a new arena allocator for the Pony language that addresses memory growth and performance issues found during stress testing, with a design inspired by snmalloc.

Comparison of Malloc() Algorithms

Hacker News Top

This article provides a detailed comparison of arena architecture in malloc() algorithms, discussing memory allocation problems in multithreaded programs and the evolution of frontend and backend designs in memory allocators.

Dense Arena Interning: The Engine of Compiler Performance

Hacker News Top

This article explains how implementing a Dense Arena Interner can drastically improve compiler performance by converting strings and structures into dense integers, enabling O(1) comparisons after an upfront hashing cost during lexing.