The Arena Allocator is a simple, yet powerful, memory management technique. However, most arena allocators leave untapped potential by only using one end. It wasn’t until recently that I discovered how you can intelligently make use of both ends of an Arena.
Key insight: reserve the front for growing data structures (strings, arrays) while allocating objects from the back. This prevents intermediate allocations from fragmenting your growing data structures at the front.
Typical Arena implementation, without dual-ended arena allocations:
= s8concat(arena, s, s8("Hello "));
s // Arena: [Hello ░░░░░░░░░░░░░░░░░░░]
char *data = allocate_data(arena); // Interrupts our string!
// Arena: [Hello [data]░░░░░░░░░░░░░]
= s8concat(arena, s, s8("World"));
s // Arena: [Hello [data]Hello World░░]
// ^Must copy "Hello"
With dual-ended arena allocations:
= s8concat(arena, s, s8("Hello "));
s // Arena: [Hello ░░░░░░░░░░░░░░░░░░░]
char *data = allocate_data(arena);
// Arena: [Hello ░░░░░░░░░░░░░[data]]
= s8concat(arena, s, s8("World")); // No relocation needed!
s // Arena: [Hello World░░░░░░░░[data]]
Neat! No copies! We effectively get a string builder without having to preallocate a block of memory up front and without any re-allocation if we would exceed that block of memory. Instead the string will naturally grow until Arena is exhausted. (Note that Depending on situation this might be a bad thing)
s8concatv
ImplementationThis implementation is heavily influenced by Chris Wellons’s design.
#define new(a, t, n) ((t *)arena_alloc(a, sizeof(t), _Alignof(t), (n)))
#define newbeg(a, t, n) ((t *)arena_alloc_beg(a, sizeof(t), _Alignof(t), (n)))
#define s8(s) (S8){(U8 *)s, countof(s)-1}
typedef struct { U8 *data; Iz len; } S8;
typedef struct { U8 *beg; U8 *end; } Arena;
static U8 *arena_alloc(Arena *a, Iz objsize, Iz align, Iz count) {
= (Uz)a->end & (align - 1);
Iz padding ((count <= (a->end - a->beg - padding) / objsize) && "out of memory");
tassert= objsize * count;
Iz total return memset(a->end -= total + padding, 0, total);
}
static U8 *arena_alloc_beg(Arena *a, Iz objsize, Iz align, Iz count) {
= -(Uz)(a->beg) & (align - 1);
Iz padding = padding + objsize * count;
Iz total (total < (a->end - a->beg) && "out of memory");
tassert*p = a->beg + padding;
U8 (p, 0, objsize * count);
memset->beg += total;
areturn p;
}
static S8 s8concatv(Arena *a, S8 head, S8 *ss, Iz count) {
= {0};
S8 r
// Check if head string is already at the front of arena
if (!head.data || (U8 *)(head.data+head.len) != a->beg) {
// Copy head to front of arena
= head;
S8 copy .data = newbeg(a, U8, head.len);
copyif (head.len) memcpy(copy.data, head.data, head.len);
= copy;
head }
// Append additional strings contiguously
for (Iz i = 0; i < count; i++) {
= ss[i];
S8 tail *data = newbeg(a, U8, tail.len);
U8 if (tail.len) memcpy(data, tail.data, tail.len);
.len += tail.len;
head}
return head;
}
The magic of s8concatv
happens in the first check: if
the head string is already positioned at the front of the arena (its end
matches a->beg
), we can append directly without copying.
This creates a contiguous result regardless of how many concatenations
we perform. And if your Arena defaults to allocating from the back,
intermediate allocations won’t interfere with future concatenations,
allowing them to continue growing in-place.
For convenience, we can also define a macro to accept variadic arguments like in the example above.
#define s8concat(arena, head, ...) \
s8concatv(arena, head, ((S8[]){__VA_ARGS__}), (countof(((S8[]){__VA_ARGS__}))))
[...]
= s8concat(arena, s8("Hello"), s8("World"));
s // Arena: [Hello World░░░░░░░░░░░░░░░░░░░]