allocators/arena

The arena allocator (also called linear allocator or bump allocator) is the simplest and fastest allocation strategy. Memory is allocated by “bumping” an offset forward. Individual allocations cannot be freed - instead, all allocations are freed at once with reset().

import "conjure/allocators/arena" as arena

When to Use Arena Allocators

Arena allocators are perfect when:

  • Many allocations share the same lifetime (e.g., all per-frame data)
  • You can bulk-free everything at once (e.g., at end of frame/level/request)
  • You want maximum allocation speed (O(1) constant time)
  • You don’t need individual deallocation

Common use cases:

  • Per-frame game data (particles, render commands, AI scratch data)
  • Per-level resources (geometry, entities, lightmaps)
  • Per-request server data (HTTP handlers, database queries)
  • Temporary string building and parsing

Creating an Arena

new

Creates a new arena allocator with the specified capacity.

// Automatic heap allocation (most common)
var myArena = arena.new(1024 * 1024)  // 1 MB arena
defer arena.destroy(&myArena)

// With provided buffer (you manage the memory)
var buffer = memory.alloc(1024 * 1024)
var myArena = arena.new(1024 * 1024, buffer)
defer memory.free(buffer)

// Using stack memory (fixed-size array)
var stackBuffer [4096]u8
var stackArena = arena.new(4096, &stackBuffer)

Signature:

func new(capacity usize, buffer rawptr = null) Arena

If buffer is null (the default), heap memory is allocated automatically.

destroy

Frees the backing memory of an arena created without a provided buffer.

arena.destroy(&myArena)

Signature:

func destroy(a *Arena)

Allocating Memory

alloc

Allocates a block of memory with default alignment (8 bytes).

var data = arena.alloc(&myArena, 256)
if data == null {
    io.println("Out of arena memory!")
}

Signature:

func alloc(a *Arena, size usize) rawptr

Returns null if there isn’t enough space remaining.

allocAligned

Allocates memory with a specific alignment.

// Allocate 16-byte aligned data for SIMD operations
var simdData = arena.allocAligned(&myArena, 64, 16)

Signature:

func allocAligned(a *Arena, size usize, alignment usize) rawptr

The alignment must be a power of 2.

allocZeroed

Allocates memory and sets all bytes to zero.

var zeroedData = arena.allocZeroed(&myArena, 1024)
// All 1024 bytes are guaranteed to be 0

Signature:

func allocZeroed(a *Arena, size usize) rawptr

allocType

Allocates memory for a specific type (convenience function).

struct Entity { x f32, y f32, health i32 }

var entity = arena.allocType[Entity](&myArena)

Signature:

func allocType[T](a *Arena) rawptr

Freeing Memory

free

Individual free is a no-op for arenas. This function exists for API consistency but does nothing.

arena.free(&myArena, ptr)  // Does nothing

Signature:

func free(a *Arena, ptr rawptr)

reset

Resets the arena, making all memory available again. This is the primary way to “free” arena memory.

// At the end of each frame
arena.reset(&frameArena)

Signature:

func reset(a *Arena)

resetZeroed

Resets the arena AND zeros all previously allocated memory.

// Clear sensitive data before reset
arena.resetZeroed(&secureArena)

Signature:

func resetZeroed(a *Arena)

Resizing Allocations

resize

Attempts to resize the most recent allocation.

var data = arena.alloc(&myArena, 100)
// Need more space...
data = arena.resize(&myArena, data, 100, 200)
if data == null {
    io.println("Not enough space to resize!")
}

Signature:

func resize(a *Arena, ptr rawptr, oldSize usize, newSize usize) rawptr

If the pointer is the most recent allocation and there’s space, this extends it in-place. Otherwise, it allocates new memory and copies the old data.

Savepoints (Temporary Memory)

Savepoints allow you to mark a position and later restore to it, effectively “freeing” all allocations made after the savepoint.

savepoint

Creates a savepoint at the current position.

var save = arena.savepoint(&myArena)

// Make temporary allocations
var temp1 = arena.alloc(&myArena, 256)
var temp2 = arena.alloc(&myArena, 512)
processData(temp1, temp2)

// Restore - temp1 and temp2 memory is now available again
arena.restore(save)

Signature:

func savepoint(a *Arena) ArenaSavepoint

restore

Restores the arena to a previous savepoint.

arena.restore(save)

Signature:

func restore(save ArenaSavepoint)

Introspection

used

Returns bytes currently allocated.

var bytesUsed = arena.used(&myArena)
io.println("Arena using #{bytesUsed} bytes")

Signature:

func used(a *Arena) usize

remaining

Returns bytes available for allocation.

var bytesLeft = arena.remaining(&myArena)

Signature:

func remaining(a *Arena) usize

capacity

Returns total arena capacity.

var total = arena.capacity(&myArena)

Signature:

func capacity(a *Arena) usize

isEmpty / isFull

Check arena state.

if arena.isEmpty(&myArena) {
    io.println("Arena has no allocations")
}

if arena.isFull(&myArena) {
    io.println("Arena is full!")
}

Signatures:

func isEmpty(a *Arena) bool
func isFull(a *Arena) bool

Types

Arena

The main arena allocator structure.

struct Arena {
    buffer rawptr      // Backing memory
    capacity usize     // Total size
    offset usize       // Current position
    prevOffset usize   // Previous position (for resize)
}

ArenaSavepoint

Represents a saved position in the arena.

struct ArenaSavepoint {
    arena *Arena       // The arena this belongs to
    offset usize       // Saved offset
    prevOffset usize   // Saved previous offset
}

Example: Per-Frame Allocation

import "conjure/allocators/arena" as arena
import "conjure/io"

var frameArena = arena.create(1024 * 1024)  // 1 MB

struct Particle { x f32, y f32, vx f32, vy f32, life f32 }
struct RenderCommand { type u8, data [64]u8 }

func gameLoop() {
    while running {
        // All frame allocations come from the arena
        var particles = *[1000]Particle(arena.alloc(&frameArena, 1000 * Particle.size))
        var commands = *[500]RenderCommand(arena.alloc(&frameArena, 500 * RenderCommand.size))

        updateParticles(particles)
        buildRenderCommands(commands)
        render(commands)

        // Single O(1) operation frees ALL frame allocations
        arena.reset(&frameArena)
    }
}

Example: Level Loading

import "conjure/allocators/arena" as arena

var levelArena = arena.create(64 * 1024 * 1024)  // 64 MB for level data

func loadLevel(path string) {
    // Reset to clear previous level
    arena.reset(&levelArena)

    // Load all level data into the arena
    var geometry = arena.alloc(&levelArena, geometrySize)
    var textures = arena.alloc(&levelArena, textureDataSize)
    var entities = arena.alloc(&levelArena, entityDataSize)
    var navmesh = arena.alloc(&levelArena, navmeshSize)

    // Parse and populate...
    loadGeometry(geometry, path)
    loadTextures(textures, path)
    // etc.
}

func unloadLevel() {
    // Everything freed instantly
    arena.reset(&levelArena)
}

Example: Nested Savepoints

import "conjure/allocators/arena" as arena

var scratch = arena.create(64 * 1024)

func processFile(path string) {
    var outerSave = arena.savepoint(&scratch)

    var fileData = arena.alloc(&scratch, fileSize)
    readFile(path, fileData)

    // Process each section
    for section in sections {
        var innerSave = arena.savepoint(&scratch)

        var sectionData = arena.alloc(&scratch, section.size)
        processSectionData(sectionData)

        // Free section-specific allocations
        arena.restore(innerSave)
    }

    // Free all file allocations
    arena.restore(outerSave)
}