Compare commits

27 Commits
old ... master

Author SHA1 Message Date
7134d6ff13 fix bug 2025-06-20 22:27:20 +02:00
786d7213ae Update README 2024-01-16 02:34:19 +01:00
59e2d3ff25 Add more tests 2024-01-16 02:22:23 +01:00
f67a3cc335 Fix arena_delete() 2024-01-16 01:38:49 +01:00
6b46af52aa Update tests
Add test for arena_attach
2024-01-15 16:33:17 +01:00
0af6582a64 Add flag option to not align allocations 2024-01-15 16:32:19 +01:00
6e0e6f7437 Add rudimentary test 2024-01-15 15:04:33 +01:00
d11557a4fe Fix bug and handle more errors
Fix bug where mprotect calls don't match the arena cap increase
(https://github.com/olemorud/arena-allocator/issues/1)

Handle syscall errors in arena_new()

Check for syscall errors in arena_delete() and switch from void to int
to return error status.
2024-01-15 15:02:43 +01:00
44a2982376 Update README 2023-08-19 01:29:21 +02:00
Ole Morud
069f6b11dc Full rewrite 2023-08-19 01:18:32 +02:00
Ole Morud
e5fa743d85 Add arena_free() 2023-08-19 00:49:51 +02:00
Ole Morud
45e6023d42 Add arena_delete() 2023-08-19 00:49:51 +02:00
Ole Morud
d3754e64e5 Rewrite everything 2023-08-19 00:49:51 +02:00
Ole Morud
57979892f8 Add arena_realloc_tail() 2023-08-19 00:49:51 +02:00
olemorud
51feaf1d00 Move header files to include/ 2023-08-19 00:49:51 +02:00
olemorud
529955dbfb Add *.so, *.a to .gitignore 2023-08-19 00:49:49 +02:00
olemorud
05e69d8776 Add dynamic and static as makefile targets 2023-08-19 00:49:49 +02:00
olemorud
9f6eaca42e Add and run WebKit based clang-format 2023-08-19 00:49:49 +02:00
olemorud
d4a9708082 Makefile: save as ar file 2023-08-19 00:49:49 +02:00
olemorud
b9d743e868 Update test_arena.c 2023-08-19 00:49:49 +02:00
olemorud
b95c63c965 Add compile_flags.txt 2023-08-19 00:49:49 +02:00
olemorud
c4fe01a54a Update arena.c, arena.h 2023-08-19 00:49:49 +02:00
olemorud
2b14b86ccb Update Makefile 2023-08-19 00:49:49 +02:00
olemorud
ba39a77c0c Add README 2023-08-19 00:49:49 +02:00
olemorud
6576032654 Makefile: update CFLAGS 2023-08-19 00:49:49 +02:00
olemorud
09528df89b Add .gitignore 2023-08-19 00:49:46 +02:00
olemorud
cd68347dd9 Remove memcpy use 2023-08-19 00:49:41 +02:00
13 changed files with 421 additions and 261 deletions

View File

@@ -1,2 +1,10 @@
BasedOnStyle: WebKit BasedOnStyle: WebKit
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: false
PointerAlignment: Right

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
obj/ obj/
test/
*.o *.o
*.a *.a
*.so *.so
build/

15
CMakeLists.txt Normal file
View File

@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.16)
project("ArenaAllocator"
VERSION 1.0
LANGUAGES C
DESCRIPTION "Arena Allocator in C"
)
add_subdirectory(src)
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
message(STATUS "Building tests")
add_subdirectory(test)
endif()

View File

@@ -1,72 +0,0 @@
all:
BUILD ?= debug
COMPILER ?= gcc
# ==== set compiler flags ====
# credits Maxim Egorushkin:
# https://stackoverflow.com/questions/48791883/best-practice-for-building-a-make-file/48793058#48793058
CC := $(COMPILER)
# -fsanitize={address,undefined}
CFLAGS.gcc.debug := -Og -ggdb -pg -fsanitize=address -fno-omit-frame-pointer
CFLAGS.gcc.release := -O3 -g -march=native -DNDEBUG
CFLAGS.gcc := -fanalyzer
CFLAGS.clang.debug :=-O0 -g3 -DBACKTRACE -rdynamic
CFLAGS.clang.release :=-O3 -g -march=native -DNDEBUG
CFLAGS.clang :=
CFLAGS := ${CFLAGS.${COMPILER}} ${CFLAGS.${COMPILER}.${BUILD}} \
-Iinclude \
-std=c2x \
-Wall -Wextra -Wpedantic -Werror
BUILD_DIR := build
BIN_DIR := lib
TEST_DIR := test
OBJS := $(BUILD_DIR)/arena.o
FPIC_OBJS := $(BUILD_DIR)/fpic/arena.o
all : static
static : $(BIN_DIR)/libarena.a
dynamic : $(BIN_DIR)/libarena.so
tests : test/test_arena
$(TEST_DIR)/test_arena : src/test_arena.c $(BIN_DIR)/libarena.a | $(TEST_DIR)
$(CC) -o $@ $(CFLAGS) $^
$(BIN_DIR)/libarena.a : $(OBJS) | $(BIN_DIR)
ar cr $@ $^
$(BIN_DIR)/libarena.so : $(FPIC_OBJS) | $(BIN_DIR)
$(CC) -o $@ -shared $(CFLAGS) $^
$(BUILD_DIR)/%.o : src/%.c | $(BUILD_DIR)
$(CC) -o $@ -c $(CFLAGS) $<
$(BUILD_DIR)/fpic/%.o : src/%.c | $(BUILD_DIR)/fpic
$(CC) -o $@ -c -fPIC $(CFLAGS) $<
$(BUILD_DIR):
mkdir -p $@
$(BUILD_DIR)/fpic:
mkdir -p $@
$(BIN_DIR) :
mkdir -p $@
$(TEST_DIR) :
mkdir -p $@
clean:
rm -rf $(BUILD_DIR) $(BIN_DIR) $(TEST_DIR)
.PHONY: clean $(BUILD_DIR) test bin all

View File

@@ -1,13 +1,65 @@
# Arena Allocator
Arena allocators are region based allocators that tie many allocations to a
single region of memory. Benefits are massivly simplified allocation and
deallocation for complex structures, increased performance due to improved
cache locality, and reduced memory fragmentation as long as individual items
don't need to be deallocated. For programs that need to micro-manage individual
allocations this is not an ideal solution.
Deallocating a region of memory with arenas is extremely fast, because the
arena length is just set to 0. Allocating memory is also extremely fast. This
implementation also grows the arena on demand using mmap and mprotect.
Extending the library with a new type of allocation strategy should be easy.
# Reference
`arena_t* arena_new()` ## `arena_t arena_new()`
Initializes and returns new arena Allocate a new arena.
`void arena_reset(arena_t *a)` If `arena_new()` fails, the struct member `data` is set to NULL. This can be
checked with `arena_new_failed()`
Free memory allocated in arena
`void* arena_alloc(arena_t *a, size_t len)` ## `bool arena_new_failed(arena_t *a)`
Returns true if creating a new arena failed
## `int arena_delete(arena_t *a)`
Delete underlying buffer of arena. Should only be used with arenas from
`arena_new()`.
Returns 0 on success, -1 on failure
## `arena_t arena_attach(void *ptr, size_t size)`
Attach an arena to an existing memory region. The arena will not expand if
capacity is exceeded.
## `void *arena_detatch(arena_t arena)`
Detach an arena from an existing memory region. Returns the underlying data.
## `void arena_reset(arena_t *a)`
Resets an arena.
## `void *arena_alloc(arena_t *a, size_t len)`
Allocate memory with an arena. Returns NULL and sets errno on failure.
## `void *arena_calloc(arena_t *a, size_t nmemb, size_t size)`
Allocate and zero memory with an arena. Returns NULL and sets errno on
failure.
Allocate new memory using arena

View File

@@ -1,24 +1,81 @@
#pragma once
#ifndef ARENA_H #ifndef ARENA_H
#define ARENA_H #define ARENA_H
#include <stddef.h> // ptrdiff_t #include <stdbool.h>
#include <stdint.h> // uintptr_t #include <stddef.h>
#define _WORD_SIZE (sizeof(intptr_t)) // flags = 0 should be considered the default
enum arena_flags {
ARENA_GROW = 1 << 0,
ARENA_DONTALIGN = 1 << 1,
};
// linked list terminology:
// https://web.archive.org/web/20230604145332/https://i.stack.imgur.com/2FbXf.png
typedef struct arena { typedef struct arena {
struct page *head, *last; char* data;
} __attribute__((aligned(_WORD_SIZE))) arena_t; size_t size;
size_t cap;
char flags;
} arena_t;
arena_t arena_new(void); /**
void arena_reset(arena_t* a); * Allocate a new arena.
void* arena_alloc(arena_t* a, size_t len); * The underlying memory is allocated with mmap.
void* arena_calloc(arena_t* a, size_t nmemb, size_t size); * Errors can be checked with `arena_new_failed()`
void* arena_realloc_tail(arena_t* a, size_t len); */
void arena_delete(arena_t* a); arena_t arena_new();
void arena_free(arena_t* a, void* p);
/**
* Delete memory mapped for arena.
* Should only be used with arenas from arena_new().
* Returns 0 on success, -1 on failure
*/
int arena_delete(arena_t *a);
/**
* Attach an arena to an existing memory region.
* The arena will not expand the region if space is exceeded.
*/
static inline arena_t arena_attach(void *ptr, size_t size)
{
return (arena_t) { .data = ptr, .size = 0, .cap = size, .flags = 0 };
}
/**
* Detach an arena from an existing memory region.
*/
static inline void *arena_detatch(arena_t arena)
{
return arena.data;
}
/**
* Returns true if creating new arena failed
*/
static inline bool arena_new_failed(arena_t *a)
{
return a->data == NULL;
}
/**
* Reset an arena.
*/
static inline void arena_reset(arena_t *a)
{
a->size = 0;
}
/**
* Allocate memory from an arena.
* Returns NULL and sets errno on failure.
*/
void *arena_alloc(arena_t *a, size_t len);
/**
* Allocate and zero memory from an arena.
* Returns NULL and sets errno on failure.
*/
void *arena_calloc(arena_t *a, size_t nmemb, size_t size);
#endif #endif

6
include/knob.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
#define KNOB_MMAP_SIZE (1UL << 36UL)
#define KNOB_ALIGNMENT (sizeof(char*))

4
src/CMakeLists.txt Normal file
View File

@@ -0,0 +1,4 @@
add_library(arena arena.c)
target_include_directories(arena PUBLIC ../include)

View File

@@ -1,158 +1,125 @@
#include "arena.h" #include "arena.h"
#include "knob.h"
#include <errno.h> #include <errno.h> // errno
#include <stdint.h> #include <stdbool.h>
#include <stdlib.h> #include <stdio.h> // fprintf
#include <string.h> #include <string.h> // strerror
#include <sys/mman.h>
#include <unistd.h> #include <unistd.h>
// (sizeof(intptr_t) isn't guaranteed to be the machine word size but on most
// compilers it is)
#define SYS_PAGE_SIZE ((size_t)sysconf(_SC_PAGE_SIZE))
#define BIG_PAGE (SYS_PAGE_SIZE + 1) #ifndef NDEBUG
#define arena_err(msg) \
fprintf(stderr, "%s (%s:%d): %s\n", msg, __func__, __LINE__, strerror(errno))
#else
#define arena_err(msg)
#endif
typedef unsigned char byte_t; static bool arena_grow(struct arena *a, size_t min_size)
struct page {
void* data;
size_t offset, prev_offset;
struct page* next;
};
// linked list terminology:
// https://web.archive.org/web/20230604145332/https://i.stack.imgur.com/2FbXf.png
arena_t arena_new(void)
{ {
struct page* p = malloc(sizeof *p); if (!(a->flags & ARENA_GROW)) {
return false;
}
if (p == NULL) size_t new_cap = a->cap * 2;
exit(errno); while (new_cap < min_size) {
new_cap *= 2;
}
p->next = NULL; int ok = mprotect(
p->offset = 0; a->data + a->cap,
p->prev_offset = 0; new_cap - a->cap,
p->data = malloc(SYS_PAGE_SIZE); PROT_READ | PROT_WRITE
);
if (p->data == NULL) if (ok == -1) {
exit(errno); arena_err("mprotect");
return false;
}
arena_t a = { a->cap = new_cap;
.head = p,
.last = p, return true;
}
arena_t arena_new()
{
size_t size = sysconf(_SC_PAGE_SIZE);
if (size == -1) {
arena_err("sysconf");
goto sysconf_failed;
}
void *p = mmap(NULL, KNOB_MMAP_SIZE, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (p == MAP_FAILED) {
arena_err("mmap");
goto mmap_failed;
}
int ok = mprotect(p, size, PROT_READ | PROT_WRITE);
if (ok == -1) {
arena_err("mprotect");
goto mprotect_failed;
}
struct arena a = {
.data = p,
.size = 0,
.cap = size,
.flags = ARENA_GROW,
}; };
return a; return a;
mprotect_failed:
ok = munmap(p, KNOB_MMAP_SIZE);
if (ok == -1) {
arena_err("munmap");
}
mmap_failed:
sysconf_failed:
return (arena_t) { 0 };
} }
void _arena_new_page(arena_t* a, size_t size) void* arena_alloc(arena_t *a, size_t size)
{ {
/* potentially reuse page from previously // align
reset arena */ if (!(a->flags & ARENA_DONTALIGN)) {
if (a->head->next != NULL) { size = (size + KNOB_ALIGNMENT - 1) & ~(KNOB_ALIGNMENT - 1);
a->head = a->head->next;
a->head->offset = 0;
a->head->prev_offset = 0;
return;
} }
void* tmp = calloc(1, sizeof *(a->head->next)); void *p = a->data + a->size;
if (a->size + size > a->cap) {
if (tmp == NULL) if (!arena_grow(a, a->size))
exit(errno); return NULL;
a->head->next = tmp;
a->head = a->head->next;
a->head->data = malloc(size);
if (a->head->data == NULL)
exit(errno);
}
void arena_reset(arena_t* a)
{
a->head = a->last;
a->head->offset = 0;
a->head->prev_offset = 0;
}
void* _arena_big_alloc(arena_t* a, size_t size)
{
_arena_new_page(a, size);
a->head->offset = BIG_PAGE;
a->head->prev_offset = BIG_PAGE;
return a->head->data;
}
void* arena_alloc(arena_t* a, size_t size)
{
if (size > SYS_PAGE_SIZE)
return _arena_big_alloc(a, size);
// align size to machine word size
size = (size + _WORD_SIZE - 1) & ~(_WORD_SIZE - 1);
if (a->head->offset > SYS_PAGE_SIZE - size) {
_arena_new_page(a, SYS_PAGE_SIZE);
return arena_alloc(a, size);
} }
a->size += size;
a->head->prev_offset = a->head->offset; return p;
a->head->offset += size;
return (byte_t*)(a->head->data) + a->head->prev_offset;
} }
void* arena_calloc(arena_t* a, size_t nmemb, size_t size) void* arena_calloc(arena_t *a, size_t nmemb, size_t size)
{ {
void* p = arena_alloc(a, nmemb * size); void *p = arena_alloc(a, nmemb * size);
if (p == NULL)
return p;
memset(p, 0, nmemb * size); memset(p, 0, nmemb * size);
return p; return p;
} }
void* arena_realloc_tail(arena_t* a, size_t len) int arena_delete(struct arena *a)
{ {
if (len <= (a->head->offset - a->head->prev_offset)) { if (!(a->flags & ARENA_GROW)) {
a->head->offset = a->head->prev_offset + len; return -1;
return (byte_t*)(a->head->data) + a->head->prev_offset;
} }
int ok = munmap(a->data, KNOB_MMAP_SIZE);
if (a->head->offset == BIG_PAGE) { if (ok == -1) {
void* tmp = realloc(a->head->data, len); arena_err("munmap");
return -1;
if (tmp == NULL)
exit(errno);
a->head->data = tmp;
return tmp;
} }
a->cap = -1;
a->head->offset = a->head->prev_offset; a->size = -1;
return 0;
return arena_alloc(a, len);
}
void arena_delete(arena_t* a)
{
struct page* p = a->last;
while (p != NULL) {
struct page* next = p->next;
free(p->data);
free(p);
p = next;
}
}
// included for completeness
void arena_free(arena_t* a, void* p)
{
(void)p;
(void)a;
return;
} }

View File

@@ -1,42 +0,0 @@
// _start test_arena_alloc.c
#include "arena.h"
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
arena_t default_arena = arena_new();
printf("\nAttempting to allocate and write to 1 byte 1024 times");
for (size_t i = 0; i < 1024; i++) {
char* c = arena_alloc(&default_arena, sizeof *c);
if (c == NULL)
err(EXIT_FAILURE, "failed to allocate memory");
*c = i & 0xFF;
}
printf("\n OK!\n");
printf("\nAttempting to allocate and write to `_SC_PAGESIZE+1` bytes");
size_t psz = sysconf(_SC_PAGESIZE) + 1;
char* c = arena_alloc(&default_arena, psz);
if (c == NULL)
err(EXIT_FAILURE, "failed to allocate memory");
for (size_t i = 0; i < psz; i++)
*c = i & 0xFF;
printf("\n OK!\n");
arena_delete(&default_arena);
return 0;
}

6
test/CMakeLists.txt Normal file
View File

@@ -0,0 +1,6 @@
include_directories(${CMAKE_SOURCE_DIR}/include)
add_executable(test_arena test_arena.c)
target_link_libraries(test_arena arena)

6
test/README Normal file
View File

@@ -0,0 +1,6 @@
run compiled test in conjunction with other tools, e.g.
/usr/bin/time strace -e trace=\!write ./test/test_arena

153
test/test_arena.c Normal file
View File

@@ -0,0 +1,153 @@
#include "arena.h"
#include "knob.h"
#include <signal.h>
#include <stdio.h> // fprintf
#include <stdlib.h> // exit, EXIT_FAILURE
#include <string.h> // memset
#include <sys/wait.h> // waitpid
#include <unistd.h> // sysconf
int main()
{
size_t page_size = sysconf(_SC_PAGE_SIZE);
if (page_size == -1) {
perror("sysconf");
exit(EXIT_FAILURE);
}
/*
* test arena from arena_new();
* ===============================
*/
{
fprintf(stderr, "creating new arena with arena_new()\n ");
arena_t a = arena_new();
if (arena_new_failed(&a)) {
fprintf(stderr, "arena_new failed\n");
exit(EXIT_FAILURE);
}
fprintf(stderr, "OK\n");
fprintf(stderr, "attempting many small allocations\n ");
for (int i = 0; i < page_size * 8; i++) {
char* s = arena_alloc(&a, 4 * sizeof *s);
if (!s) {
fprintf(stderr, "arena_alloc failed\n");
exit(EXIT_FAILURE);
}
memset(s, 'a', 4);
}
fprintf(stderr, "OK\n");
fprintf(stderr, "testing allocations of cap * 3 + 123\n ");
for (int i = 0; i < 2; i++) {
size_t n = a.cap * 3 + 123;
volatile char* s = arena_alloc(&a, n);
if (!s) {
fprintf(stderr, "arena_alloc failed\n");
exit(EXIT_FAILURE);
}
}
fprintf(stderr, "OK\n");
fprintf(stderr, "calling arena_delete() on arena from arena_new()\n ");
int ok = arena_delete(&a);
if (ok == -1) {
fprintf(stderr, "arena_delete failed\n");
exit(EXIT_FAILURE);
}
fprintf(stderr, "OK\n");
}
/*
* test arena made by arena_attach();
* ===============================
*/
{
fprintf(stderr, "creating new arena with arena_attach() and malloc()\n ");
char* p = malloc(page_size);
if (!p) {
perror("malloc");
exit(EXIT_FAILURE);
}
arena_t a = arena_attach(p, page_size);
fprintf(stderr, "OK\n");
bool failed = false;
fprintf(stderr, "try to grow more than the buffer size (should fail)\n ");
int i;
for (i=0; i < page_size + 1; i++) {
char* s = arena_alloc(&a, 8);
if (!s) {
failed = true;
break;
}
}
if (!failed) {
fprintf(stderr, "allocation was supposed to fail, but didn't\n");
exit(EXIT_FAILURE);
} else if (i != page_size / KNOB_ALIGNMENT ) {
fprintf(stderr, "allocation failed after %d allocations, but should fail after %d!\n", i, page_size/sizeof(void*));
exit(EXIT_FAILURE);
}
fprintf(stderr, "OK\n");
fprintf(stderr, "deleting an arena not made with arena_new (should fail)\n ");
int ok = arena_delete(&a);
if (ok != -1) {
fprintf(stderr, "arena_delete was supposed to fail, but didn't\n");
exit(EXIT_FAILURE);
}
fprintf(stderr, "OK\n");
free(arena_detatch(a));
}
/*
* test the memory protection
* ===============================
*/
{
fprintf(stderr, "attempting to access memory beyond arena cap (should sigsegv)\n ");
pid_t p;
switch (p = fork()) {
case -1: /* error */
perror("fork()");
exit(EXIT_FAILURE);
case 0: /* child */
arena_t a = arena_new();
if (arena_new_failed(&a)) {
fprintf(stderr, "arena_new failed\n");
exit(EXIT_FAILURE);
}
char* s = arena_alloc(&a, 12345);
if (!s) {
fprintf(stderr, "arena_alloc failed\n");
exit(EXIT_FAILURE);
}
// should sigsegv
a.data[a.cap] = 1;
exit(EXIT_SUCCESS);
}
int status;
int ok = waitpid(p, &status, 0);
if (!ok) {
perror("waitpid");
}
if (!( WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)) {
fprintf(stderr, "allocating beyond arena cap succeeded when it shouldn't!\n");
exit(EXIT_FAILURE);
}
fprintf(stderr, "OK\n");
}
return EXIT_SUCCESS;
}