This commit is contained in:
Nils O. Selåsdal
2026-05-11 22:37:01 +02:00
commit c9a51a85ea
14 changed files with 809 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
---
BasedOnStyle: Google
AccessModifierOffset: "-4"
AllowShortFunctionsOnASingleLine: Empty
BreakBeforeBraces: WebKit
IndentWidth: "4"
ColumnLimit: 160
BreakConstructorInitializers: AfterColon
ConstructorInitializerAllOnOneLineOrOnePerLine: true
SortIncludes: false
PointerAlignment: Right
# clang-format 9: AlignConsecutiveMacros: true
# clang-format 11: AlignConsecutiveBitFields: true
# clang-format 14: PackConstructorInitializers: Never
...
+3
View File
@@ -0,0 +1,3 @@
build/
.*swp
.gdb_history
+13
View File
@@ -0,0 +1,13 @@
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${default}",
"${workspaceFolder}/include"
],
"defines": []
}
],
"version": 4
}
+14
View File
@@ -0,0 +1,14 @@
{
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.associations": {
"*.h": "c",
},
"C_Cpp.default.includePath": [
"/opt/homebrew/include/",
"$(workspaceFolder)/include/",
],
"C_Cpp.default.cStandard": "gnu17"
}
+15
View File
@@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "make: build",
"command": "make",
"group": {
"kind": "build",
"isDefault": true
},
}
]
}
+55
View File
@@ -0,0 +1,55 @@
CC = gcc
LD = $(CC)
CFLAGS = -Wall -Wextra -Wno-unused-parameter -ggdb
CPPFLAGS += -Iinclude
LDFLAGS +=
ifndef DEBUG
CFLAGS += -O2
CPPFLAGS += -DNDEBUG
else
CPPFLAGS += -DDEBUG
endif
BUILDDIR = build
SOURCES = $(wildcard src/*.c)
OBJECTS = $(addprefix $(BUILDDIR)/,$(SOURCES:.c=.o))
DEPS = $(addprefix $(BUILDDIR)/,$(SOURCES:.c=.d))
LIB = liblilalloc
TEST_SOURCES = $(wildcard test/*.c)
TEST_BINS = $(patsubst test/%.c,$(BUILDDIR)/test/%,$(TEST_SOURCES))
.PHONY: all
all: $(BUILDDIR)/$(LIB).a
$(BUILDDIR)/%.o: %.c
mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c -o $@ $<
$(BUILDDIR)/$(LIB).a: $(OBJECTS)
ar crs $@ $^
$(BUILDDIR)/test/%: test/%.c $(BUILDDIR)/$(LIB).a
mkdir -p $(@D)
$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -o $@ $< $(BUILDDIR)/$(LIB).a
.PHONY: test
test: $(TEST_BINS)
@set -e; \
for test_bin in $(TEST_BINS); do \
echo "Running $$test_bin:" ; \
./$$test_bin; \
echo ; \
done
.PHONY: clean
clean:
rm -rf $(BUILDDIR)
ifeq (,$(findstring clean, $(MAKECMDGOALS)))
-include $(DEPS)
endif
+53
View File
@@ -0,0 +1,53 @@
#ifndef LALLOC_H_
#define LALLOC_H_
#include <stdint.h>
#include <stddef.h>
#ifdef __ccplusplus
extern "C" {
#endif
// Internal header added in front of each allocation
typedef struct BlockHeader BlockHeader;
struct BlockHeader {
uint32_t next; // offset from base
uint32_t prev; // offset from base
uint32_t size_and_flags; // size | free_bit
// Padding to ensure we always return 16 byte aligned pointers.
uint8_t pad[4];
};
_Static_assert(sizeof(BlockHeader) == 16, "sizeof(BlockHeader) is not 16");
/**
* General purpose allocator.
* - Allocated pointers are aligned to 16 bytes.
* - Backing buffer must be aligned to 16 bytes start
* - 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
* fragmentation to a minimum
*/
typedef struct LAlloc LAlloc;
struct LAlloc {
uint8_t *base; // backing store
uint32_t size; // total size
BlockHeader *heap_start; // first block
BlockHeader *next_alloc; // start of free block search
};
void lalloc_init(LAlloc *allocator, void *backing_store, uint32_t size);
void *lalloc(LAlloc *allocator, uint32_t size);
void lfree(LAlloc *allocator, void *ptr);
BlockHeader *block_from_offset(const LAlloc *allocator, uint32_t offset);
uint32_t block_size(const BlockHeader *block);
int block_is_free(const BlockHeader *block);
#ifdef __ccplusplus
} // extern "C"
#endif
#endif
+180
View File
@@ -0,0 +1,180 @@
#ifndef LILARENA__H_
#define LILARENA__H_
#include <stddef.h>
#include <stdint.h>
#include <assert.h>
/** @file
* Arena allocator.
*
* An LArena allocates memory from a provided buffer.
*
* The backing buffer should be suitable aligned to the types
* that will be allocated from it. Use .e.g
* @code
* _Alignas(16) uint8_t mem[1024];
* @endcode
* as the backing buffer.
*
* Normally the arena is used to allocate memory from, reset and reuse the arena.
*
* Temporary allocations can be obtained and released back to he arena via
* larena_tmp_begin()/larena_tmp_end() pair of calls, releassing memory from all
* lalloc() calls between larena_tmp_begin() and larena_tmp_end()the back to the arena.
*
*/
typedef struct LArena LArena;
struct LArena {
uint8_t *start; // start of user buffer
uint8_t *curr; // next alloc point
uint8_t *end; // one past end of user buffer
int tmp_allocs; // level of larena_tmp_begin stacks
};
typedef struct LArenaTemp LArenaTemp;
struct LArenaTemp {
uint8_t *alloc_point;
LArena *arena;
};
/** Initialize a LArena with predefined backing array.
*
* @data must be an array type
* Use as
* @code
* _Alignas(16) uint8_t v[128];
* LArena a = LARENA_STATIC_INITIALIZER(v);
* @endcode
*/
#define LARENA_STATIC_INITIALIZER(data) LARENA_INITIALIZER((data), sizeof(data))
/** Initializer for LArena with predefined memory
* Use as
* @code
* uint8_t v[128];
* LArena a = LARENA_INITIALIZER(v, sizeof v);
* @endcode
* or
* @code
* uint8_t *m = malloc(128);
* LArena a = LARENA_INITIALIZER(m, 128);
* @endcode
*/
#define LARENA_INITIALIZER(data, sz) {.start = (data), .curr = (data), .end = (data) + sz, .tmp_allocs = 0}
/** Initialize a LArena with an array as the backing memory to allocate from.
* The macro can be used at local scope to allocate memory automatic on the stack,
* or at global scope where the backing array will have static storage duration
*
* Use as:
* @code
* LARENA_DEFINE(my_allocator, 1024, 16)
* struct foo *foo = larena_alloc(&my_allocator);
* if (foo == NULL) {
* // out of memory
* }
* @endcode
*
* @param var_name Variable name for the allocator
* @param size_ byte size of the backing array
* @param alignment_ alignment of the backing array
*/
#define LARENA_DEFINE(var_name, size_, alignment_) \
struct { \
_Alignas(alignment_) uint8_t memory[size_]; \
} var_name##_arena_memory; \
LArena var_name = LARENA_STATIC_INITIALIZER(var_name##_arena_memory.memory)
/** Allocate space for @type_ , returned memory is aligned to requirements
* of @type.
*
* Use as:
* @code
* LARENA_DEFINE(my_allocator, 1024)
* struct foo *foo = LARENA_ALLOC_TYPE(&my_allocator, struct foo);
* // Array types can be used too:
* struct Foo *str = LARENA_ALLOC_TYPE(&my_allocator, struct Foo[32]);
* @endcode
*/
#define LARENA_ALLOC_TYPE(allocator, type_) larena_alloc_aligned((allocator), sizeof(type_), _Alignof(type_))
// Arena size in bytes
static inline size_t larena_size(const LArena *arena)
{
return (size_t)(arena->end - arena->start);
}
// Used bytes of the arena
static inline size_t larena_used(const LArena *arena)
{
return (size_t)(arena->curr - arena->start);
}
// Available unused bytes in the arena
static inline size_t larena_available(const LArena *arena)
{
return larena_size(arena) - larena_used(arena);
}
// Initialize an LArena to allocate from @mem which is @sz bytes
static inline void larena_init(LArena *arena, void *memory, size_t sz)
{
arena->start = arena->curr = (uint8_t *)memory;
arena->end = arena->start + sz;
}
// begin temporary allocation. All allocations afterwards will be released in larena_tmp_end
static inline LArenaTemp larena_tmp_begin(LArena *arena)
{
LArenaTemp state = {.alloc_point = arena->curr, .arena = arena};
arena->tmp_allocs++;
return state;
}
// end temporary allocations, releasing all allocations since the most recent larena_tmp_begin
static inline void larena_tmp_end(LArenaTemp *state)
{
LArena *arena = state->arena;
assert(arena->tmp_allocs > 0);
arena->curr = state->alloc_point;
arena->tmp_allocs--;
}
// Allocate @sz bytes from the LArena. Returns NULL if not enough space
static inline void *larena_alloc(LArena *arena, size_t sz)
{
if (sz > larena_available(arena)) {
return NULL;
}
void *start = arena->curr;
arena->curr += sz;
return start;
}
// Allocate @sz bytes from the LArena, returned pointer is aligned to @align
// Returns NULL if not enough space
static inline void *larena_alloc_aligned(LArena *arena, size_t sz, unsigned int align) [[gnu::alloc_align(3)]]
{
assert(align != 0);
assert((align & (align - 1)) == 0); // power of 2
uintptr_t curr = (uintptr_t)arena->curr;
uintptr_t aligned = (curr + (align - (uintptr_t)1)) & ~(align - (uintptr_t)1);
uint8_t *start = (uint8_t *)aligned;
if (sz > (larena_available(arena))) {
return NULL;
}
arena->curr = start + sz;
return start;
}
// reset arena, making all memory available for allocation
// also invalidates any LArenaTemp instances.
static inline void larena_reset(LArena *arena)
{
arena->curr = arena->start;
}
#endif
+66
View File
@@ -0,0 +1,66 @@
#ifndef LPOOL_H_
#define LPOOL_H_
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef union LPoolBlock LPoolBlock;
union LPoolBlock {
LPoolBlock *next; // next free block (when not allocated)
unsigned char data[0]; // data when allocated
};
/** @file
* Pool allocator.
*
* An LPool allocates fixed size memory chunks from a provided buffer,
* and memory allocations can be free'd in any order.
*
* The backing array should be suitable aligned to the types
* that will be allocated from it. Use .e.g
* @code
* _Alignas(16) char mem[1024];
* @endcode
* as the backing buffer.
*
*/
typedef struct LPool LPool;
struct LPool {
LPoolBlock *free_list; // free list
LPoolBlock *start; // start of user buffer
LPoolBlock *end; // one past end of user buffer
uint32_t block_size; // aligned size of each block
uint32_t capacity; // number of blocks
uint32_t align; // alignment
};
/** Initialize the pool.
*
* Block size will be rounded up by alignment, or to size of a pointer if alignment is less that a pointer.
* Prefer to use lock sizes that's pointer aligned
*
* @param buffer backing buffer for the pool
* @param buffer_size size of @buffer
* @param block_size size of each allocation.
* @param alignment alignment of each block (must be power of 2)
*/
void lpool_init(LPool *pool, void *buffer, size_t buffer_size, uint32_t block_size, uint32_t alignment);
// Allocate a block from the pool. Returns NULL if no more space
[[gnu::assume_aligned(_Alignof(LPoolBlock))]]
void *lpool_alloc(LPool *pool);
// Free @p . Returns memory to the pool
void lpool_free(LPool *pool, void *p);
// Frees all memory in the pool, resetting it to initial state.
void lpool_reset(LPool *pool);
#ifdef __cplusplus
} // extern "C"
#endif
#endif
+170
View File
@@ -0,0 +1,170 @@
#include <stdint.h>
#include <stddef.h>
#include <assert.h>
#include "lalloc.h"
#define ALIGNMENT sizeof(BlockHeader)
#define ALIGN_UP(n) (((n) + (__typeof__(n))(ALIGNMENT) - 1) & ~((__typeof__(n))(ALIGNMENT) - 1))
#define ALIGN_DOWN(n) ((n) & ~((__typeof__(n))(ALIGNMENT) - 1))
#define NULL_BLOCK (0xffffffff)
#define BLOCK_FREE ((uint32_t)1)
static inline void block_set_free(BlockHeader *block)
{
block->size_and_flags |= BLOCK_FREE;
}
static inline void block_set_used(BlockHeader *block)
{
block->size_and_flags &= ~BLOCK_FREE;
}
static inline void block_set_size(BlockHeader *block, uint32_t size)
{
uint32_t flags = block->size_and_flags & BLOCK_FREE;
block->size_and_flags = size | flags;
}
uint32_t block_size(const BlockHeader *block)
{
return block->size_and_flags & ~BLOCK_FREE;
}
int block_is_free(const BlockHeader *block)
{
return (block->size_and_flags & BLOCK_FREE) != 0;
}
inline BlockHeader *block_from_offset(const LAlloc *allocator, uint32_t offset)
{
if (offset == NULL_BLOCK) {
return NULL;
}
return (BlockHeader *)((uintptr_t)allocator->base + offset);
}
static inline uint32_t bh_offset(LAlloc *allocator, BlockHeader *block)
{
if (block == NULL) {
return NULL_BLOCK;
}
return (uint32_t)((uintptr_t)block - (uintptr_t)allocator->base);
}
static void maybe_split_block(LAlloc *allocator, BlockHeader *block, uint32_t size)
{
// assumes size is already aligned
uint32_t bsize = block_size(block);
if (bsize < size + sizeof(BlockHeader) + ALIGNMENT) {
return;
}
uint8_t *base = (uint8_t *)block;
BlockHeader *new_block = (BlockHeader *)(base + sizeof(BlockHeader) + size);
uint32_t new_size = bsize - size - sizeof(BlockHeader);
block_set_size(block, size);
block_set_used(block);
new_block->size_and_flags = new_size | BLOCK_FREE;
new_block->next = block->next;
new_block->prev = bh_offset(allocator, block);
BlockHeader *next = block_from_offset(allocator, new_block->next);
if (next) {
next->prev = bh_offset(allocator, new_block);
}
block->next = bh_offset(allocator, new_block);
}
static void merge_with_next(LAlloc *allocator, BlockHeader *block)
{
BlockHeader *next = block_from_offset(allocator, block->next);
if (next == NULL || !block_is_free(next)) return;
uint32_t new_size = block_size(block) + sizeof(BlockHeader) + block_size(next);
block_set_size(block, new_size);
block->next = next->next;
BlockHeader *nextnext = block_from_offset(allocator, next->next);
if (nextnext) nextnext->prev = bh_offset(allocator, block);
// note - caller have to adjust next_alloc on return
}
void lalloc_init(LAlloc *allocator, void *backing_store, uint32_t size)
{
// backing store must be aligned, otherwise all alignment guarantees are lies
assert(((uintptr_t)backing_store & (ALIGNMENT - 1)) == 0);
assert(size >= sizeof(BlockHeader));
assert(size < 0xfffffff0);
allocator->base = backing_store;
allocator->size = size;
BlockHeader *first = backing_store;
// allocator logic requires the first block to be properly aligned
// as well as size being eligned.
uint32_t usable = ALIGN_DOWN(size) - sizeof(BlockHeader);
block_set_free(first);
block_set_size(first, usable);
first->next = NULL_BLOCK;
first->prev = NULL_BLOCK;
allocator->heap_start = first;
allocator->next_alloc = first;
}
void *lalloc(LAlloc *allocator, uint32_t size)
{
if (size > UINT32_MAX - (ALIGNMENT - 1)) {
return NULL;
}
size = ALIGN_UP(size);
BlockHeader *start = allocator->next_alloc ? allocator->next_alloc : allocator->heap_start;
BlockHeader *curr = start;
do {
if (block_is_free(curr) && block_size(curr) >= size) {
maybe_split_block(allocator, curr, size);
block_set_used(curr);
BlockHeader *next = block_from_offset(allocator, curr->next);
allocator->next_alloc = next ? next : allocator->heap_start;
return (uint8_t *)curr + sizeof(BlockHeader);
}
BlockHeader *next = block_from_offset(allocator, curr->next);
curr = next ? next : allocator->heap_start;
} while (curr != start);
return NULL;
}
void lfree(LAlloc *allocator, void *ptr)
{
if (!ptr) {
return;
}
BlockHeader *block = (BlockHeader *)((uint8_t *)ptr - sizeof(BlockHeader));
assert((uint8_t *)ptr - sizeof(BlockHeader) >= allocator->base && (uint8_t *)ptr < allocator->base + allocator->size);
// double free detection
assert(!block_is_free(block));
block_set_free(block);
merge_with_next(allocator, block);
BlockHeader *prev = block_from_offset(allocator, block->prev);
if (prev != NULL && block_is_free(prev)) {
merge_with_next(allocator, prev);
allocator->next_alloc = prev;
} else {
allocator->next_alloc = block;
}
}
+109
View File
@@ -0,0 +1,109 @@
#include <assert.h>
#include <string.h>
#include "lpool.h"
#define ALIGN_UP(n, alignment) (((n) + (__typeof__(n))(alignment) - 1) & ~((__typeof__(n))(alignment) - 1))
void lpool_init(LPool *pool, void *buffer, size_t buffer_size, uint32_t block_size, uint32_t align)
{
assert(align > 0);
assert((align & (align - 1)) == 0); // power of 2
// enforce minimum for storing free list pointers
if (align < _Alignof(LPoolBlock)) {
align = _Alignof(LPoolBlock);
}
if (block_size < sizeof(LPoolBlock)) {
block_size = sizeof(LPoolBlock);
}
// ensure initial alignment
uint32_t stride = ALIGN_UP(block_size, align);
// align first usable block
uintptr_t start_addr = (uintptr_t)buffer;
uintptr_t first_addr = ALIGN_UP(start_addr, align);
size_t padding = first_addr - start_addr;
if (padding + stride > buffer_size) {
pool->capacity = 0;
pool->free_list = NULL;
} else {
size_t usable = buffer_size - padding;
pool->capacity = usable / stride;
// build intrusive free list
LPoolBlock *block = (LPoolBlock *)first_addr;
pool->free_list = block;
for (uint32_t i = 0; i < pool->capacity - 1; i++) {
LPoolBlock *next = __builtin_assume_aligned(&block->data[0] + stride, _Alignof(LPoolBlock));
block->next = next;
block = next;
}
block->next = NULL; // last element
}
pool->block_size = stride;
pool->align = align;
pool->start = buffer;
pool->end = buffer + buffer_size;
}
void *lpool_alloc(LPool *pool)
{
if (!pool->free_list) {
return NULL;
}
LPoolBlock *block = pool->free_list;
pool->free_list = block->next;
#ifdef DEBUG
memset(block, 0xCB, pool->block_size);
#endif
return block->data;
}
void lpool_free(LPool *pool, void *p)
{
if (p == NULL) {
return;
}
LPoolBlock *block = p;
#ifdef DEBUG
assert(block >= pool->start && block < pool->end);
assert(((uintptr_t)p - (uintptr_t)pool->start) % pool->block_size == 0);
// double-free detection
for (LPoolBlock *f = pool->free_list; f; f = f->next) {
if (f == p) {
assert(0 && "Double free detected in pool!");
return;
}
}
memset(p, 0xDD, pool->block_size);
#endif
// When not in use, store pointer to next free element inline in the block
block->next = pool->free_list;
pool->free_list = p;
}
void lpool_reset(LPool *pool)
{
if (pool->capacity == 0) return;
uintptr_t start_addr = (uintptr_t)pool->start;
uintptr_t first_addr = ALIGN_UP(start_addr, pool->align);
// build intrusive free list
LPoolBlock *p = (LPoolBlock *)first_addr;
for (size_t i = 0; i < pool->capacity - 1; ++i) {
LPoolBlock *next = __builtin_assume_aligned(&p->data[0] + pool->block_size, _Alignof(LPoolBlock));
p->next = next;
p = next;
}
p->next = NULL; // last element
pool->free_list = (LPoolBlock *)first_addr;
}
+50
View File
@@ -0,0 +1,50 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "lalloc.h"
void debug_alloc(const LAlloc *allocator)
{
puts("--debug start--");
for (BlockHeader *b = allocator->heap_start; b; b = block_from_offset(allocator, b->next)) {
printf("Block size %u free %d prev %u next %u \n", block_size(b), block_is_free(b), b->prev, b->next);
}
puts("--debug end--");
}
[[gnu::aligned(16)]] char buff[1023];
int main(void)
{
printf("bh size %zu\n", sizeof(BlockHeader));
#define SZ 64
LAlloc a;
char *ptrs[SZ];
lalloc_init(&a, buff, sizeof(buff));
debug_alloc(&a);
ptrs[0] = lalloc(&a, 1023 - 16 * 2 - 144);
ptrs[1] = lalloc(&a, 80);
ptrs[2] = lalloc(&a, 32);
debug_alloc(&a);
lfree(&a, ptrs[0]);
ptrs[3] = lalloc(&a, 12);
ptrs[4] = lalloc(&a, 12);
debug_alloc(&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_alloc(&a);
}
+33
View File
@@ -0,0 +1,33 @@
#include <stdio.h>
#include "larena.h"
static void test_arena_define(void)
{
puts(__func__);
LARENA_DEFINE(arena, 1024, 32);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
larena_alloc(&arena, 1000);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
LArenaTemp tmp1 = larena_tmp_begin(&arena);
larena_alloc(&arena, 20);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
LArenaTemp tmp2 = larena_tmp_begin(&arena);
larena_alloc(&arena, 4);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
larena_tmp_end(&tmp2);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
larena_tmp_end(&tmp1);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
larena_reset(&arena);
printf("size=%zu available=%zu\n", larena_size(&arena), larena_available(&arena));
}
int main(int argc, char *argv[])
{
test_arena_define();
return 0;
}
+32
View File
@@ -0,0 +1,32 @@
#include <stdio.h>
#include "lpool.h"
struct TestData {
float x[3];
float y[3];
float z[3];
float w[3];
};
int main(int argc, char *argv[])
{
_Alignas(16) uint8_t store[2048];
LPool pool;
lpool_init(&pool, store, sizeof store, sizeof(struct TestData), _Alignof(struct TestData));
printf("align=%u capacity=%u block_size=%u\n", pool.align, pool.capacity, pool.block_size);
struct TestData *data = lpool_alloc(&pool);
size_t allocs = 1;
for (;;) {
struct TestData *t = lpool_alloc(&pool);
if (t == NULL) {
break;
}
allocs++;
}
printf("Total allocs = %zu\n", allocs);
printf("alloc succeded = %d\n", lpool_alloc(&pool) != NULL);
lpool_free(&pool, data);
printf("alloc succeded = %d\n", lpool_alloc(&pool) != NULL);
return 0;
}