From 41a2987ed17a24470a7f66bdd86531a93f784b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20O=2E=20Sel=C3=A5sdal?= Date: Sat, 19 Sep 2026 20:45:33 +0200 Subject: [PATCH] Add a buddy allocator --- src/base/lalloc.c | 174 ++++++++++++++++++++++++++++++++++++++++++++++ src/base/lalloc.h | 41 +++++++++++ src/base/pool.h | 2 +- src/main.c | 45 ++++++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 src/base/lalloc.c create mode 100644 src/base/lalloc.h diff --git a/src/base/lalloc.c b/src/base/lalloc.c new file mode 100644 index 0000000..724a4c3 --- /dev/null +++ b/src/base/lalloc.c @@ -0,0 +1,174 @@ +#include "lalloc.h" + +#define BLOCK_ALIGNMENT sizeof(LBlockHeader) +#define BLOCK_SPLIT_MIN_SIZE BLOCK_ALIGNMENT +#define NULL_BLOCK (0xffffffff) +#define BLOCK_FREE ((U32)1) + +static inline void lblock_set_free(LBlockHeader *block) +{ + block->size_and_flags |= BLOCK_FREE; +} + +static inline void lblock_set_used(LBlockHeader *block) +{ + block->size_and_flags &= ~BLOCK_FREE; +} + +static inline void lblock_set_size(LBlockHeader *block, U32 size) +{ + U32 flags = block->size_and_flags & BLOCK_FREE; + Assert((size & BLOCK_FREE) == 0); + block->size_and_flags = size | flags; +} + +U32 lblock_size(const LBlockHeader *block) +{ + return block->size_and_flags & ~BLOCK_FREE; +} + +int lblock_is_free(const LBlockHeader *block) +{ + return (block->size_and_flags & BLOCK_FREE) != 0; +} + +LBlockHeader *lblock_from_offset(const LAlloc *allocator, U32 offset) +{ + if (offset == NULL_BLOCK) { + return NULL; + } + + return (LBlockHeader *)((UMM)allocator->base + offset); +} + +static inline U32 lbh_offset(LAlloc *allocator, LBlockHeader *block) +{ + if (block == NULL) { + return NULL_BLOCK; + } + + return (U32)((uintptr_t)block - (uintptr_t)allocator->base); +} + +static void lmaybe_split_block(LAlloc *allocator, LBlockHeader *block, U32 size) +{ + // this function assumes size is already aligned + + U32 bsize = lblock_size(block); + if (size > U32_Max - (sizeof(LBlockHeader) + BLOCK_SPLIT_MIN_SIZE)) { + return; // size is so large that size + header + min would overflow U32 anyway + } + if (bsize < size + sizeof(LBlockHeader) + BLOCK_SPLIT_MIN_SIZE) { + return; // no gain from splitting this block + } + + U8 *base = (U8 *)block; + LBlockHeader *new_block = (LBlockHeader *)(base + size + sizeof(LBlockHeader) ); + + U32 new_size = bsize - size - sizeof(LBlockHeader); + + lblock_set_size(block, size); + lblock_set_used(block); + lblock_set_size(new_block, new_size); + lblock_set_free(new_block); + new_block->next = block->next; + new_block->prev = lbh_offset(allocator, block); + + LBlockHeader *next = lblock_from_offset(allocator, new_block->next); + if (next) { + next->prev = lbh_offset(allocator, new_block); + } + + block->next = lbh_offset(allocator, new_block); +} + +static void lmerge_with_next(LAlloc *allocator, LBlockHeader *block) +{ + LBlockHeader *next = lblock_from_offset(allocator, block->next); + if (next == NULL || !lblock_is_free(next)) { + return; + } + + // block and next are both existing blocks within the same backing store, whose + // total size is asserted in lalloc_init to be < 0xfffffff0, so this can't overflow + // unless allocator state is already corrupted. + Assert(lblock_size(block) <= U32_Max - sizeof(LBlockHeader) - lblock_size(next)); + U32 new_size = lblock_size(block) + sizeof(LBlockHeader) + lblock_size(next); + + lblock_set_size(block, new_size); + block->next = next->next; + LBlockHeader *nextnext = lblock_from_offset(allocator, next->next); + if (nextnext) nextnext->prev = lbh_offset(allocator, block); + // note - caller have to adjust next_alloc on return +} + +void lalloc_init(LAlloc *allocator, void *backing_store, U32 size) +{ + // backing store must be aligned, otherwise all alignment guarantees are lies + + Assert(IsAlignedTo((UMM)backing_store, BLOCK_ALIGNMENT)); + Assert(size >= sizeof(LBlockHeader)); + Assert(size < 0xfffffff0); + + allocator->base = backing_store; + allocator->size = size; + + LBlockHeader *first = backing_store; + // We need to keep size aligned to at least 2 as we reuse the LSB for the FREE flag + U32 usable = AlignDown(size, 2) - sizeof(LBlockHeader); + lblock_set_free(first); + lblock_set_size(first, usable); + first->next = NULL_BLOCK; + first->prev = NULL_BLOCK; + + allocator->next_alloc = first; +} + +void *lalloc(LAlloc *allocator, U32 size) +{ + if (size > U32_Max - (BLOCK_ALIGNMENT - 1)) { // -1 so AlignUp below doesn't overflow + return NULL; + } + size = AlignUp(size, BLOCK_ALIGNMENT); + + LBlockHeader *start = allocator->next_alloc ? allocator->next_alloc : (LBlockHeader *)allocator->base; + LBlockHeader *curr = start; + + do { + if (lblock_is_free(curr) && lblock_size(curr) >= size) { + lmaybe_split_block(allocator, curr, size); + lblock_set_used(curr); + LBlockHeader *next = lblock_from_offset(allocator, curr->next); + allocator->next_alloc = next ? next : (LBlockHeader *)allocator->base; + return (U8 *)curr + sizeof(LBlockHeader); + } + LBlockHeader *next = lblock_from_offset(allocator, curr->next); + curr = next ? next : (LBlockHeader *)allocator->base; + } while (curr != start); + + return NULL; +} + +void lfree(LAlloc *allocator, void *ptr) +{ + if (ptr == NULL) { + return; + } + + LBlockHeader *block = (LBlockHeader *)((U8 *)ptr - sizeof(LBlockHeader)); + + Assert((U8 *)block >= allocator->base && (U8 *)ptr < allocator->base + allocator->size); + // double free detection + Assert(!lblock_is_free(block)); + + lblock_set_free(block); + lmerge_with_next(allocator, block); + + LBlockHeader *prev = lblock_from_offset(allocator, block->prev); + if (prev != NULL && lblock_is_free(prev)) { + lmerge_with_next(allocator, prev); + allocator->next_alloc = prev; + } else { + allocator->next_alloc = block; + } +} diff --git a/src/base/lalloc.h b/src/base/lalloc.h new file mode 100644 index 0000000..fdc5bea --- /dev/null +++ b/src/base/lalloc.h @@ -0,0 +1,41 @@ +#pragma once +#include "core.h" + +// Internal header added in front of each allocation +typedef struct LBlockHeader LBlockHeader; +struct LBlockHeader { + U32 next; // offset from base + U32 prev; // offset from base + U32 size_and_flags; // size | free_bit + // Padding to ensure we always return 16 byte aligned pointers. + uint8_t pad[4]; +}; +_Static_assert(sizeof(LBlockHeader) == 16, "sizeof(BlockHeader) is not 16"); + +/** + * General purpose allocator from a pre-defined backing store + * - Allocated pointers are aligned to 16 bytes. + * - Backing buffer must be aligned to 16 bytes + * - 4Gb - 16 is max for the allocator backing buffer. + * - Allocation of > 0 bytes returns a size aligned up to 16 bytes + * - Allocation of 0 bytes returns a valid pointer with 0 usable space + * The allocator maintains a doubly linked list of blocks, + * when blocks are free'd, adjacent blocks are merged to keep. + * This is also known as a buddy allocator + * fragmentation to a minimum + */ +typedef struct LAlloc LAlloc; +struct LAlloc { + uint8_t *base; // backing store + U32 size; // total size + LBlockHeader *next_alloc; // start of free block search +}; + +void lalloc_init(LAlloc *allocator, void *backing_store, U32 size); + +void *lalloc(LAlloc *allocator, U32 size); +void lfree(LAlloc *allocator, void *ptr); + +LBlockHeader *lblock_from_offset(const LAlloc *allocator, U32 offset); +U32 lblock_size(const LBlockHeader *block); +int lblock_is_free(const LBlockHeader *block); diff --git a/src/base/pool.h b/src/base/pool.h index 60ec658..eed02b9 100644 --- a/src/base/pool.h +++ b/src/base/pool.h @@ -32,7 +32,7 @@ struct APoolCreateOptions { apool_create_ex(arena, &(APoolCreateOptions){sizeof(type), alignof(type), n_reserved, 0}) APool *apool_create_ex(Allocator *allocator, const APoolCreateOptions *options); void *apool_alloc(APool *pool); -INLINE void apool_allocz(APool *pool) +INLINE void *apool_allocz(APool *pool) { void *ptr = apool_alloc(pool); if (ptr != NULL) { diff --git a/src/main.c b/src/main.c index 7633869..9098fc2 100644 --- a/src/main.c +++ b/src/main.c @@ -4,6 +4,7 @@ #include "base/platform.h" #include "base/pool.h" #include "base/string.h" +#include "base/lalloc.h" #include #include @@ -689,6 +690,49 @@ void string_test_dlist(void) } +void debug_lalloc(const LAlloc *allocator) +{ + puts("--debug start--"); + for (LBlockHeader *b = (LBlockHeader *)allocator->base; b; b = lblock_from_offset(allocator, b->next)) { + printf("Block size %u free %d prev %u next %u \n", lblock_size(b), lblock_is_free(b), b->prev, b->next); + } + puts("--debug end--"); +} + +static void lalloc_test_basic(void) +{ + printf("bh size %zu\n", sizeof(LBlockHeader)); +#define SZ 64 + LAlloc a; + char *ptrs[SZ] = {}; + alignas(16) char buff[1023]; + lalloc_init(&a, buff, sizeof(buff)); + debug_lalloc(&a); + ptrs[0] = lalloc(&a, 1023 - 16 * 2 - 144); + ptrs[1] = lalloc(&a, 80); + ptrs[2] = lalloc(&a, 32); + debug_lalloc(&a); + lfree(&a, ptrs[0]); + ptrs[3] = lalloc(&a, 12); + ptrs[4] = lalloc(&a, 12); + debug_lalloc(&a); + + lfree(&a, ptrs[1]); + lfree(&a, ptrs[2]); + lfree(&a, ptrs[3]); + lfree(&a, ptrs[4]); + + for (int i = 0; i < SZ; i++) { + ptrs[i] = lalloc(&a, rand() % 256 + 1); + } + + for (int i = 0; i < SZ; i++) { + // if (rand() % 2 == 0) { + lfree(&a, ptrs[i]); + // } + } + debug_lalloc(&a); +} int main(int argc, char *argv[]) { arena_test_1(); @@ -709,6 +753,7 @@ int main(int argc, char *argv[]) string_test_hex(); string_test_slist(); string_test_dlist(); + lalloc_test_basic(); }