Arena Allocators in C

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

Arena Allocators in C

Oct 11, 2026 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.

2026年10月11日,C语言中的区域分配器。众所周知,C语言是一门非常底层的语言。它允许(甚至在某种程度上强迫)你自行管理所使用的内存。这大概是人们在日常编程中转向其他语言的众多原因之一。在本文中,我将展示一种内存管理范式,它不仅性能卓越,而且极其通用。它能显著简化任务,并大幅降低内存泄漏的风险。也许你已经了解它们,也许没有,无论如何:我所说的是区域分配器(arena allocators)。在进入实现细节之前,我先简要介绍一下区域分配器的概念。

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:

什么是区域分配器?人们通常管理内存的方式是:每次需要时,都从堆中通过 malloc 分配所需的小块内存。如果后来发现需要的内存比最初计划的多,他们会使用 realloc 来增加分配的内存大小。一旦使用完毕,他们会调用 free 来告诉操作系统该内存不再需要,可以用于其他任务。这很好,但存在几个问题:

  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. 程序员必须记住在用完内存后将其释放。如果分配发生在循环中(例如在游戏中),这就特别危险,因为在这种情况下,操作系统会不断分配越来越多的内存,但这些内存却从未被释放。内存作为一种资源并非无限。在某个时刻,你的计算机内存会耗尽,程序会被操作系统终止(即崩溃)。

  3. Allocating and freeing many small chunks of memory is slow.

  4. 分配和释放许多小块内存的速度很慢。

  5. 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.

  6. 如果你有一个类似这样的函数:i32 *create_array(usize len) { i32 *elements = (i32 *)malloc(sizeof(int)); return elements; }。现在告诉我:调用该函数的程序员如何知道此函数执行了内存分配?在这种情况下,人们可以通过查看函数名称并思考其功能来推断,但这并不总是那么简单。记住,这一点很重要,因为程序员需要知道何时释放内存。必须让程序员意识到释放内存是他的责任。

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: The memory lives for the duration of the entire program; The memory lives until the completion of some tasks (i.e. a frame in a game or the handling of an HTTP request); 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.

解决方案。引入区域分配器。大多数生命周期(即某块内存从使用到释放的时间长度)可以归纳为几类:内存存活于整个程序运行期间;内存存活于某些任务完成之前(例如游戏中的一帧或处理一个 HTTP 请求);内存存活于函数作用域内。这通常已经足够分类了,但根据你对“组”的定义,可能还会有更多。无论如何,我们可以分配一大块内存,用于处理具有相似生命周期的对象(即可以一起释放的对象)。这样我们就拥有了大量可用的内存。每当我们使用其中的一部分内存时,就推进一个指针,以记录内存使用到了什么位置,以及空闲内存从哪里开始。一旦你看到实现代码,这一点就会变得更加清晰。

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 individual 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 typedefs 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.

实现。说明:在开始之前,当使用 C 语言时,我几乎总是在代码顶部包含此文件。它只是对一些内置类型进行了 typedef,并包含了一些有用的常用工具。我直接包含 .c 文件,因为我使用统一构建(unity builds)以加快编译速度。如果你愿意,也可以轻松地为这些代码编写头文件。

// types.c … (code omitted) … 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:

// types.c …(代码省略)… arena.c:这是实际的区域分配器实现。我们从包含保护开始(同样是因为统一构建),并包含我们将要使用的一些 libc 头文件:

#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 { void *data; usize size; usize used; };。它跟踪已使用了多少内存。这也可以通过使用一个指向内存中尚未使用的区域起始位置的指针来实现。size 字段用于判断分配时是否溢出了区域(如果你使用的内存过多,这种情况很有可能发生)。而 data 只是指向区域的起始位置。到目前为止一切顺利,但现在我们需要一种创建新区域的方法。如下所示:

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

(代码块保持原样)当返回多个值时,与其使用输出参数(这……)