Compare commits

22 Commits
master ... old

Author SHA1 Message Date
Ole Morud
cf964c4dcc optimize shrinking reallocs 2023-06-17 12:14:28 +02:00
Ole Morud
b3638c3c00 Update makefile and fix analzer complaints 2023-06-05 22:36:42 +02:00
Ole Morud
1fd96ff1ce Fix arena_tail_realloc memory bug
`arena_realloc_tail` exceeding PAGE_SIZE would create a big page,
preceeding calls to `arena_realloc_tail` would then cause memory bugs.
2023-06-05 22:26:10 +02:00
Ole Morud
0cde95de64 Rewrite arena as linked list of pages 2023-06-05 22:26:10 +02:00
Ole Morud
ac8193eb8b Remove print statement 2023-06-05 22:26:08 +02:00
Ole Morud
279618f52d Add arena_free() 2023-06-05 22:26:04 +02:00
Ole Morud
90f72e9593 Add arena_delete() 2023-06-05 22:26:00 +02:00
Ole Morud
c71788ba72 Rewrite everything 2023-06-05 22:26:00 +02:00
Ole Morud
f8c0db9937 Add arena_realloc_tail() 2023-06-03 11:37:06 +02:00
olemorud
bf73807e40 Move header files to include/ 2023-06-02 18:06:27 +02:00
olemorud
680936296f add *.so, *.a to .gitignore 2023-05-15 23:55:55 +02:00
olemorud
7ccb5bed74 Add dynamic and static as makefile targets 2023-05-15 23:04:44 +02:00
olemorud
42cb5d0c4e Add and run WebKit based clang-format 2023-05-15 22:25:54 +02:00
olemorud
98b67e1a53 Makefile: save as ar file 2023-05-15 21:19:53 +02:00
olemorud
846ef41cd4 Update test_arena.c 2023-05-15 20:00:34 +02:00
olemorud
e0a98b0f08 Add compile_flags.txt 2023-05-15 20:00:22 +02:00
olemorud
22fb643c0f Update arena.c, arena.h 2023-05-15 20:00:12 +02:00
olemorud
d5fea00170 Update Makefile 2023-05-15 19:57:56 +02:00
olemorud
1fad6fed5d Add README 2023-05-15 01:04:37 +02:00
olemorud
e0e363a189 Makefile: update CFLAGS 2023-05-15 01:02:09 +02:00
olemorud
b35f63ccc5 add .gitignore 2023-05-15 01:02:09 +02:00
olemorud
6d9059aefc remove memcpy use 2023-05-15 01:02:09 +02:00
13 changed files with 262 additions and 422 deletions

View File

@@ -1,10 +1,2 @@
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/

View File

@@ -1,15 +0,0 @@
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()

72
Makefile Normal file
View File

@@ -0,0 +1,72 @@
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,65 +1,13 @@
# 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()`
Allocate a new arena. Initializes and returns new arena
If `arena_new()` fails, the struct member `data` is set to NULL. This can be `void arena_reset(arena_t *a)`
checked with `arena_new_failed()`
Free memory allocated in arena
## `bool arena_new_failed(arena_t *a)` `void* arena_alloc(arena_t *a, size_t len)`
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,81 +1,24 @@
#pragma once
#ifndef ARENA_H #ifndef ARENA_H
#define ARENA_H #define ARENA_H
#include <stdbool.h> #include <stddef.h> // ptrdiff_t
#include <stddef.h> #include <stdint.h> // uintptr_t
// flags = 0 should be considered the default #define _WORD_SIZE (sizeof(intptr_t))
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 {
char* data; struct page *head, *last;
size_t size; } __attribute__((aligned(_WORD_SIZE))) arena_t;
size_t cap;
char flags;
} arena_t;
/** arena_t arena_new(void);
* Allocate a new arena. void arena_reset(arena_t* a);
* The underlying memory is allocated with mmap. void* arena_alloc(arena_t* a, size_t len);
* Errors can be checked with `arena_new_failed()` void* arena_calloc(arena_t* a, size_t nmemb, size_t size);
*/ void* arena_realloc_tail(arena_t* a, size_t len);
arena_t arena_new(); void arena_delete(arena_t* a);
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

View File

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

View File

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

View File

@@ -1,125 +1,158 @@
#include "arena.h" #include "arena.h"
#include "knob.h"
#include <errno.h> // errno #include <errno.h>
#include <stdbool.h> #include <stdint.h>
#include <stdio.h> // fprintf #include <stdlib.h>
#include <string.h> // strerror #include <string.h>
#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))
#ifndef NDEBUG #define BIG_PAGE (SYS_PAGE_SIZE + 1)
#define arena_err(msg) \
fprintf(stderr, "%s (%s:%d): %s\n", msg, __func__, __LINE__, strerror(errno))
#else
#define arena_err(msg)
#endif
static bool arena_grow(struct arena *a, size_t min_size) typedef unsigned char byte_t;
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)
{ {
if (!(a->flags & ARENA_GROW)) { struct page* p = malloc(sizeof *p);
return false;
}
size_t new_cap = a->cap * 2; if (p == NULL)
while (new_cap < min_size) { exit(errno);
new_cap *= 2;
}
int ok = mprotect( p->next = NULL;
a->data + a->cap, p->offset = 0;
new_cap - a->cap, p->prev_offset = 0;
PROT_READ | PROT_WRITE p->data = malloc(SYS_PAGE_SIZE);
);
if (ok == -1) { if (p->data == NULL)
arena_err("mprotect"); exit(errno);
return false;
}
a->cap = new_cap; arena_t a = {
.head = p,
return true; .last = p,
}
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_alloc(arena_t *a, size_t size) void _arena_new_page(arena_t* a, size_t size)
{ {
// align /* potentially reuse page from previously
if (!(a->flags & ARENA_DONTALIGN)) { reset arena */
size = (size + KNOB_ALIGNMENT - 1) & ~(KNOB_ALIGNMENT - 1); if (a->head->next != NULL) {
a->head = a->head->next;
a->head->offset = 0;
a->head->prev_offset = 0;
return;
} }
void *p = a->data + a->size; void* tmp = calloc(1, sizeof *(a->head->next));
if (a->size + size > a->cap) {
if (!arena_grow(a, a->size)) if (tmp == NULL)
return NULL; exit(errno);
}
a->size += size; a->head->next = tmp;
return p;
a->head = a->head->next;
a->head->data = malloc(size);
if (a->head->data == NULL)
exit(errno);
} }
void* arena_calloc(arena_t *a, size_t nmemb, size_t size) void arena_reset(arena_t* a)
{ {
void *p = arena_alloc(a, nmemb * size); a->head = a->last;
if (p == NULL) a->head->offset = 0;
return p; 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->head->prev_offset = a->head->offset;
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* p = arena_alloc(a, nmemb * size);
memset(p, 0, nmemb * size); memset(p, 0, nmemb * size);
return p; return p;
} }
int arena_delete(struct arena *a) void* arena_realloc_tail(arena_t* a, size_t len)
{ {
if (!(a->flags & ARENA_GROW)) { if (len <= (a->head->offset - a->head->prev_offset)) {
return -1; a->head->offset = a->head->prev_offset + len;
return (byte_t*)(a->head->data) + a->head->prev_offset;
} }
int ok = munmap(a->data, KNOB_MMAP_SIZE);
if (ok == -1) { if (a->head->offset == BIG_PAGE) {
arena_err("munmap"); void* tmp = realloc(a->head->data, len);
return -1;
if (tmp == NULL)
exit(errno);
a->head->data = tmp;
return tmp;
} }
a->cap = -1;
a->size = -1; a->head->offset = a->head->prev_offset;
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;
} }

42
src/test_arena.c Normal file
View File

@@ -0,0 +1,42 @@
// _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;
}

View File

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

View File

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

View File

@@ -1,153 +0,0 @@
#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;
}