Memory Arena Tutorial in C

How to create a simple arena allocator in C.

Updated March 16, 2024

Table of contents

Quick Start

// cl main.c

#include <stdio.h>
#include <stdlib.h>

#define GB (1024 * 1024 * 1024) // 1 gigabyte

/*
---------- Arena ----------
Hello, 0World!0000000000000
^       ^      ^
str     str2   offset
*/

typedef struct {
    unsigned char *data;
    size_t length;
    size_t offset;
} Arena;

Arena arena_alloc(size_t sz) {
    unsigned char *memory = malloc(sz);
    Arena arena = {
        .data = memory,
        .length = sz,
        .offset = 0
    };
    return arena;
}

void *arena_push(Arena *a, size_t sz) {
    if(a->offset+sz <= a->length) {
        void *p = &a->data[a->offset];
        a->offset += sz;
        memset(p, 0, sz);
        return p;
    }   
    printf("Game over, man. Game over!"); // Handle out of memory.
    return NULL;
}  

int main() {
    
    Arena arena = arena_alloc(GB);
    
    char *str = arena_push(&arena, 8 * sizeof(char));
    memcpy(str, "Hello, ", 7);
    str[7] = '\0';
    
    char *str2 = arena_push(&arena, 7 * sizeof(char));
    memcpy(str2, "World!", 6);
    str2[6] = '\0';
    
    printf("%s%s", str, str2);
    
    // output: Hello, World!
    
    return 0;
}

Tutorial

Introduction

One straightforward way to handle memory is to allocate a large contiguous chunk of memory when the program starts and then use that chunk throughout the program's lifetime. When the program exits, the memory is released.

Arena struct

Create a file named main.c and add these lines to it:

#include <stdio.h>
#include <stdlib.h>

#define GB (1024 * 1024 * 1024)

typedef struct {
    unsigned char *data;
    size_t length;
    size_t offset;
} Arena;
  • unsigned char is typically used as buffer type for raw memory manipulation.
  • size_t is an unsigned integer type guaranteed to be large enough to contain the size of the largest possible object on the system. It is often used in memory allocation and array indexing.
  • length represents the total size of the available memory in the arena, measured in number of bytes.
  • offset is used to obtain the next available memory slot in the arena, measured in number of bytes.

arena_alloc

Add these lines:

Arena arena_alloc(size_t sz) {
    unsigned char *memory = malloc(sz);
    Arena arena = {
        .data = memory,
        .length = sz,
        .offset = 0
    };
    return arena;
}

int main() { 
    Arena arena = arena_alloc(GB);
    return 0;
}

Arena arena = arena_alloc(GB); creates a new arena that can hold 1 gigabyte.

arena_push

arena_push allocates sz bytes from the Arena and returns the address of the allocated location:

void *arena_push(Arena *a, size_t sz) {
    if(a->offset+sz <= a->length) {
        void *p = &a->data[a->offset];
        a->offset += sz;
        memset(p, 0, sz);
        return p;
    }   
    printf("Game over, man. Game over!"); // Handle out of memory.
    return NULL;
}   
  • if(a->offset+sz <= a->length) { checks whether the requested allocation size sz fits in the arena. This simple allocator doesn't handle memory resizing.
  • void *p = &a->data[a->offset] obtains an address to the next available slot and stores it in p. Initially, the offset is zero, so it points to the beginning of the arena.
  • a->offset += sz advances the offset, making it to point to the next available memory slot.
  • memset(p, 0, sz) zero-initializes the memory.
  • return p returns the address to the memory we just allocated.
  • If the requested size doesn't fit in the arena, we simply print out a message and return NULL. You should handle this scenario better.

Usage

Here is how you can use the Arena:

int main() {
  
    // Create arena.
    Arena arena = arena_alloc(GB); 

    // Allocate memory.
    char *str = arena_push(&arena, 8 * sizeof(char)); 
    
    // In C++, you must cast the return value explicitly:
    // char *str = (char *)arena_push(&arena, 8 * sizeof(char));
    
    // Store data.
    memcpy(str, "Hello, ", 7); 
    str[7] = '\0';
    
    // Allocate more memory.
    char *str2 = arena_push(&arena, 7 * sizeof(char));

    // Store more data.
    memcpy(str2, "World!", 6);
    str2[6] = '\0';
    
    printf("%s%s", str, str2);
    // output: Hello, World!

    return 0;
}

The two strings are stored contiguously in the memory, like this:

---------- Arena ----------
Hello, 0World!0000000000000
^       ^      ^
str     str2   offset

Closing remarks

This tutorial introduces a very basic allocator. It doesn't handle out-of-memory situations or memory reuse.

Why not simply use malloc() every time you want to allocate memory? Because it's not free to allocate and release memory, and it can also become cumbersome to manage lifetimes.

In this example, the allocation process simply involves returning a pointer to the next available slot, and we are not manually freeing memory; the entire memory chunk is released at once when the program exits.

What about garbage-collected languages? One issue with those is that the collection process may occur at inconvenient times. This can be problematic in applications where predictable results are expected, such as in games. However, you can also use arena-type memory management strategies in those languages as well.

Leave a comment

You can use Markdown to format your comment.