Add expandable arena implementation. Add option for 0xCD Allocator memory initialization

This commit is contained in:
Musa Mahmood 2025-11-25 05:33:18 -05:00
parent 6daa9d89fb
commit 7bb81077bd
14 changed files with 261 additions and 52 deletions

View File

@ -2,6 +2,16 @@
#include "Base.h"
#define ALLOCATOR_DEBUG_MODE 0
#define ALLOCATOR_POISON_MEMORY_ON_ALLOCATION \
(BUILD_DEBUG && ALLOCATOR_DEBUG_MODE)
#if ALLOCATOR_POISON_MEMORY_ON_ALLOCATION
#define ALLOCATOR_INIT_VALUE 0xCD
#else
#define ALLOCATOR_INIT_VALUE 0xCD
#endif
enum class Allocator_Mode: s32 {
ALLOCATE = 0,
RESIZE = 1,

View File

@ -1,8 +1,6 @@
#include "Arena.h"
#include "Arena_Windows.cpp"
constexpr u16 ARENA_DEFAULT_ALIGNMENT = CPU_REGISTER_WIDTH_BYTES;
// For arrays, use `Array`, which is backed by the general purpose allocator
// or use `ArenaArray` if you need to expand the size to an unknown size.
void* arena_allocator_proc (Allocator_Mode mode, s64 requested_size, s64 old_size, void* old_memory, void* allocator_data) {
@ -13,7 +11,7 @@ void* arena_allocator_proc (Allocator_Mode mode, s64 requested_size, s64 old_siz
return arena_alloc(arena, requested_size);
} break;
case Allocator_Mode::RESIZE: {
Assert(false); // DO NOT USE RESIZE WITH ARENAS!
Assert(false); // DO NOT USE RESIZE WITH ARENAS! :ArenaResizing
// Or maybeeee...
// Resize should check if current_point matches the end of the old allocation?
// and resize accordingly + pass back same pointer.
@ -102,12 +100,15 @@ void arena_set_bootstrap_flag (Arena* arena) { arena->flags |= Arena_Flags::Is_B
void arena_reset_keeping_memory (Arena* arena) {
if (!is_valid(arena)) return;
arena->current_point = arena_start(arena);
}
void arena_reset (Arena* arena) {
if (!is_valid(arena)) return;
arena->current_point = arena_start(arena);
free_pages_down_to(arena, arena->initial_commit_page_count);
}
@ -116,6 +117,8 @@ void arena_reset_overwriting_memory (Arena* arena, Memory_Wipe_Function wipe_fun
Assert(wipe_function != nullptr);
if (wipe_function == nullptr) return;
wipe_function(arena_start(arena), (u64)(arena->current_point - arena_start(arena)));
arena_reset(arena);
}
void* arena_alloc (Arena* arena, s64 byte_count) {
@ -174,10 +177,30 @@ s64 reserve_size (Arena_Reserve ar) {
return 0;
}
constexpr s64 Arena_Sizes[6] = {
64LL * 1024,
2LL * 1024 * 1024,
64LL * 1024 * 1024,
2LL * 1024 * 1024 * 1024,
64LL * 1024 * 1024 * 1024,
2LL * 1024 * 1024 * 1024 * 1024,
};
Arena_Reserve next_reserve_size (s64 size) {
for (u8 i = 0; i < 6; i += 1) {
if (size <= Arena_Sizes[i]) {
return (Arena_Reserve)i;
}
}
return Arena_Reserve::Size_64T;
}
// arena_usage_bytes is kinda pointless tbh.
s64 arena_usage_bytes (Arena* arena) { return (s64)(arena->current_point - arena_start(arena)); }
s64 arena_usage_committed_bytes (Arena* arena) { return (s64)(arena->first_uncommitted_page - arena->memory_base); }
// for arena details, I need to setup my string builder first.
Allocator get_allocator(Arena* arena) {
Allocator get_allocator (Arena* arena) {
return { arena_allocator_proc, arena };
}

View File

@ -1,10 +1,14 @@
#pragma once
struct ExpandableArena; // fwd declare #temp
#if OS_WINDOWS
constexpr u32 ARENA_DEFAULT_COMMIT_PAGE_COUNT = 16; // 16 * 4k page = 64kB
constexpr s64 ARENA_DEFAULT_COMMIT_SIZE_BYTES = 65536;
#endif
constexpr u16 ARENA_DEFAULT_ALIGNMENT = CPU_REGISTER_WIDTH_BYTES;
#define ARENA_DEBUG BUILD_DEBUG
enum class Arena_Reserve: u8 {
@ -87,7 +91,7 @@ s64 arena_usage_committed_bytes (Arena* arena);
s64 reserve_size (Arena* arena);
s64 reserve_size (Arena_Reserve ar);
bool is_valid (Arena* arena);
Allocator get_allocator(Arena* arena);
Allocator get_allocator (Arena* arena);
// Platform-Specific Implementations (forward-declared)
void platform_init (Arena* arena, s64 new_reserve);
@ -95,14 +99,20 @@ void extend_committed_pages (Arena* arena, u8* end);
void free_pages_down_to (Arena* arena, s64 pages_to_keep);
void arena_delete (Arena* arena);
Arena_Reserve next_reserve_size (s64 size);
struct Auto_Reset {
Arena* arena;
u8* starting_point;
Auto_Reset(Arena* arena) {
Assert(is_valid(arena));
this->arena = arena;
this->starting_point = arena->current_point;
Assert(is_valid(arena));
}
Auto_Reset(ExpandableArena* arena_ex) {
Auto_Reset((Arena*)arena_ex);
}
~Auto_Reset() {
@ -137,6 +147,10 @@ struct Push_Alignment { // #rename to Arena_Push_Alignment?
this->arena->alignment = alignment;
}
Push_Alignment(ExpandableArena* arena_ex, u16 alignment) {
Push_Alignment((Arena*)arena_ex, alignment);
}
~Push_Alignment() {
arena->alignment = original_alignment;
}

View File

@ -1,6 +1,7 @@
// API in Arena.h
#include <mutex>
global b64 arena_tables_initialized = false;
global std::mutex arena_table_mutex;
global s32 arenas_in_flight_count[6];
global Array<Arena*> arena_free_table[6];
@ -15,6 +16,8 @@ void initialize_arena_table () {
array_reserve(arena_free_table[i], 64);
array_reserve(arenas_in_flight[i], 64);
}
arena_tables_initialized = true;
}
Arena* next_arena (Arena_Reserve reserve_size) {

View File

@ -32,7 +32,7 @@ void extend_committed_pages (Arena* arena, u8* end) {
void free_pages_down_to (Arena* arena, s64 pages_to_keep) {
if (arena == nullptr) return;
Assert(pages_to_keep >= 0);
Assert(pages_to_keep >= 1); // Always keep one page because we bootstrap a lot.
s64 bytes_to_keep = pages_to_keep * PLATFORM_MEMORY_PAGE_SIZE;
if (bytes_to_keep >= reserve_size(arena)) {
Assert(false); // Break in debug builds, but release we just do nothing.

View File

@ -178,12 +178,16 @@ force_inline s64 Next_Power_Of_Two(s64 v) {
// usage `Auto_Reset guard(arena);` within a scope.
#define auto_reset(x) \
Auto_Reset Concat(_auto_reset_guard_, __LINE__)(x)
#define push_allocator(x) Push_Allocator Concat(_push_alloc_guard_, __LINE__)(x)
#define push_allocator(x) \
Push_Allocator Concat(_push_alloc_guard_, __LINE__)(x)
#define push_alignment(x, y) \
Push_Alignment Concat(_push_align_guard_, __LINE__)(x, y)
#define push_arena(x) \
Push_Arena Concat(_push_alloc_guard_, __LINE__)(x)
#define auto_release_temp() auto_release(get_temp_allocator());
Push_Arena Concat(_push_arena_guard_, __LINE__)(x)
#define push_expandable_arena(x) \
Push_Expandable_Arena Concat(_push_ex_arena_guard_, __LINE__)(x)
#define auto_release_temp() \
auto_release(get_temp_allocator());
#define auto_release(x) \
Auto_Release Concat(_auto_release_guard_, __LINE__)(x)

View File

@ -1,22 +1,4 @@
// See Context_Base in jai, and TCTX in raddebugger:
struct Thread_Context {
Arena* temp; // Used for temporary allocations, scratch space.
Arena* arena; // general purpose local arena
Allocator allocator;
s32 thread_idx;
u16 _padding0;
u16 GPAllocator_alignment = 16;
// Logger logger;
// Stack_Trace* stack_trace;
// #TODO: other debug information
// #TODO:
// Array<Thread*> threads_created; // maybe should be linked-list?
// Thread* thread_that_created_me = nullptr; // so we can remove from above array
// Mutex thread_context_mutex;
string thread_name;
};
Thread_Context* get_thread_context();
@ -24,6 +6,14 @@ struct Push_Arena {
Thread_Context* context;
Allocator original_allocator;
Push_Arena(ExpandableArena* arena_ex) {
Assert(is_valid(arena_ex));
context = get_thread_context();
Assert(context != nullptr);
original_allocator = context->allocator;
context->allocator = get_allocator(arena_ex);
}
Push_Arena(Arena* arena) {
Assert(is_valid(arena));
context = get_thread_context();
@ -76,7 +66,7 @@ force_inline void set_thread_context (Thread_Context* new_context) {
thread_local_context = new_context;
}
Thread_Context* get_thread_context() {
Thread_Context* get_thread_context () {
return (Thread_Context*)thread_local_context;
}
@ -91,10 +81,10 @@ force_inline Allocator get_context_allocator() {
void temp_reset_keeping_memory() {
Thread_Context* context = get_thread_context();
arena_reset_keeping_memory(context->temp);
arena_reset(context->temp, false);
}
void temp_reset() {
Thread_Context* context = get_thread_context();
arena_reset(context->temp);
arena_reset(context->temp, true);
}

View File

@ -0,0 +1,20 @@
struct Thread_Context {
ExpandableArena* temp; // Used for temporary allocations, scratch space.
ExpandableArena* arena; // general purpose local arena
Allocator allocator;
s32 thread_idx;
u16 _padding0;
u16 GPAllocator_alignment = 16;
// Logger logger;
// Stack_Trace* stack_trace;
// #TODO: other debug information
// #TODO:
// Array<Thread*> threads_created; // maybe should be linked-list?
// Thread* thread_that_created_me = nullptr; // so we can remove from above array
// Mutex thread_context_mutex;
string thread_name;
};
Thread_Context* get_thread_context ();

View File

@ -0,0 +1,115 @@
ExpandableArena* expandable_arena_new (Arena_Reserve starting_reserve, s32 commit_page_count) {
ExpandableArena* new_arena = (ExpandableArena*)bootstrap_arena(starting_reserve, commit_page_count);
// Note: beyond first 32 bytes ExpandableArena will not be initialized, so we do it here:
new_arena->current = (Arena*)new_arena;
new_arena->current_point = expandable_arena_start(new_arena);
new_arena->next_arenas = Array<Arena*>(); // next_arenas will be uninitialized, so we have to do this
new_arena->next_arenas.allocator = get_allocator(new_arena);
array_reserve(new_arena->next_arenas, 8);
return new_arena;
}
void* expandable_arena_allocator_proc (Allocator_Mode mode, s64 requested_size, s64 old_size, void* old_memory, void* allocator_data) {
ExpandableArena* arena = (ExpandableArena*)allocator_data;
Assert(arena != nullptr);
switch (mode) {
case Allocator_Mode::ALLOCATE: {
return expandable_arena_alloc(arena, requested_size);
} break;
case Allocator_Mode::RESIZE: {
// See note :ArenaResizing
void* new_memory = expandable_arena_alloc(arena, requested_size);
memcpy(new_memory, old_memory, old_size);
return new_memory;
} break;
case Allocator_Mode::DEALLOCATE:
return nullptr;
break;
}
return nullptr;
}
bool is_valid (ExpandableArena* arena) {
return (arena != nullptr)
&& (arena->memory_base != nullptr)
&& (arena->current != nullptr);
}
void* expandable_arena_alloc (ExpandableArena* arena_ex, s64 byte_count) {
Assert(arena_ex != nullptr);
Assert(arena_ex->memory_base != nullptr); // must be initialized before calling.
Assert(is_valid(arena_ex));
Assert(arena_tables_initialized);
Arena* arena = (Arena*)arena_ex->current;
u8* result = Align<u8*>(arena->current_point, arena->alignment);
u8* result_end = result + byte_count;
if (result_end > arena->first_uncommitted_page) {
if (result_end > arena_address_limit(arena)) {
// Pick an appropriate reserve size that will fit this allocation.
Arena_Reserve new_min_reserve = next_reserve_size(byte_count);
if (arena->reserve_size > new_min_reserve) {
new_min_reserve = arena->reserve_size;
}
Arena* new_arena = next_arena(new_min_reserve);
new_arena->alignment = arena_ex->alignment;
new_arena->flags = arena_ex->flags;
arena_ex->current = new_arena;
array_add(arena_ex->next_arenas, new_arena);
// Get to actual allocation:
result = Align<u8*>(new_arena->current_point, new_arena->alignment);
result_end = result + byte_count;
if (result_end > arena_address_limit(new_arena)) {
extend_committed_pages(new_arena, result_end);
}
} else {
extend_committed_pages(arena, result_end);
}
}
arena_ex->current->current_point = result_end;
return result;
}
u8* expandable_arena_start (ExpandableArena* arena_ex) {
return Align(arena_ex->memory_base + sizeof(ExpandableArena), ARENA_DEFAULT_ALIGNMENT);
}
Allocator get_allocator (ExpandableArena* arena_ex) {
return { expandable_arena_allocator_proc, arena_ex };
}
void arena_reset (ExpandableArena* arena_ex, bool free_extra_pages) {
if (!is_valid(arena_ex)) return;
// Free arenas in `next_arenas`
for (s64 i = 0; i < arena_ex->next_arenas.count; i += 1) {
release_arena(arena_ex->next_arenas[i], free_extra_pages);
}
// Reset next_arenas
array_reset_keeping_memory(arena_ex->next_arenas);
arena_ex->current_point = expandable_arena_start(arena_ex);
if (free_extra_pages) {
free_pages_down_to((Arena*)arena_ex, arena_ex->initial_commit_page_count);
}
}
force_inline void arena_delete (ExpandableArena* arena_ex) {
arena_reset(arena_ex, true);
arena_delete((Arena*)arena_ex);
}

View File

@ -0,0 +1,26 @@
// Just an idea I had so that we can start with a small arena and increase on an as-needed basis,
// this way allocations are always extremely fast.
// Note that this downcasts to Arena, so can be initialized in the same way.
// DO NOT USE push_arena WITH THIS! IT WILL CALL THE WRONG PROC!
struct ExpandableArena {
u8* current_point = nullptr;
u8* memory_base = nullptr;
u8* first_uncommitted_page = nullptr;
u16 alignment = CPU_REGISTER_WIDTH_BYTES;
Arena_Reserve reserve_size = Arena_Reserve::Size_64K;
Arena_Flags flags = Arena_Flags::None;
u32 initial_commit_page_count = ARENA_DEFAULT_COMMIT_PAGE_COUNT;
// 32 bytes up to here
Arena* current;
Array<Arena*> next_arenas; // 40 B
};
ExpandableArena* expandable_arena_new (Arena_Reserve starting_reserve=Arena_Reserve::Size_64K, s32 commit_page_count=8);
void* expandable_arena_allocator_proc (Allocator_Mode mode, s64 requested_size, s64 old_size, void* old_memory, void* allocator_data);
bool is_valid (ExpandableArena* arena);
void* expandable_arena_alloc (ExpandableArena* arena_ex, s64 byte_count);
u8* expandable_arena_start (ExpandableArena* arena_ex);
Allocator get_allocator (ExpandableArena* arena_ex);
void arena_reset (ExpandableArena* arena_ex, bool free_extra_pages=true);
force_inline void arena_delete (ExpandableArena* arena_ex);

View File

@ -101,7 +101,7 @@ void* GPAllocator_New (s64 new_size, s64 alignment, bool initialize) {
auto memory = Aligned_Alloc(new_size, alignment);
// _aligned_malloc does not zero memory, so we can zero it here
if (initialize && memory) { memset(memory, 0, new_size); }
if (initialize && memory) { memset(memory, ALLOCATOR_INIT_VALUE, new_size); }
Add_Allocation(new_size, memory, (s32)alignment);
return memory;
}
@ -117,7 +117,7 @@ void* GPAllocator_Resize (s64 old_size, void* old_memory, s64 new_size, s64 alig
auto new_memory_address = Aligned_Realloc(old_size, old_memory, new_size, alignment);
if (initialize && new_memory_address && new_size > old_size) {
memset((u8*)new_memory_address + old_size, 0, new_size - old_size);
memset((u8*)new_memory_address + old_size, ALLOCATOR_INIT_VALUE, new_size - old_size);
}
Remove_Allocation(old_memory);
Add_Allocation(new_size, new_memory_address, (s32)alignment);

View File

@ -4,11 +4,12 @@ internal void Bootstrap_Main_Thread_Context () {
// 1. Setup arena table
initialize_arena_table();
// 2. Setup thread local context
Arena* arena = next_arena(Arena_Reserve::Size_64G);
thread_local_context = New<Thread_Context>(get_allocator(arena));
thread_local_context->temp = next_arena(Arena_Reserve::Size_64G);
thread_local_context->arena = arena;
thread_local_context->allocator = get_allocator(arena);
ExpandableArena* arena_ex = expandable_arena_new(Arena_Reserve::Size_64M, 16);
thread_local_context = New<Thread_Context>(get_allocator(arena_ex));
thread_local_context->temp = expandable_arena_new(Arena_Reserve::Size_2M, 16);
thread_local_context->arena = arena_ex;
thread_local_context->allocator = get_allocator(arena_ex);
thread_local_context->thread_idx = 0;
thread_local_context->thread_name = "Main Thread";
// thread_local_context->logger = init_logger();
@ -38,9 +39,11 @@ void run_arena_array_tests () {
internal void Main_Entry_Point (int argc, WCHAR **argv) {
run_arena_array_tests();
Worker_Info* info = (Worker_Info*)GPAllocator_New(sizeof(Worker_Info), 64);
// Worker_Info* info = (Worker_Info*)GPAllocator_New(sizeof(Worker_Info), 64);
debug_break();
printf("sizeof(Array): %zd\n", sizeof(Array<f32>));
printf("sizeof(Arena): %zd\n", sizeof(Arena));
printf("sizeof(Worker_Info): %zd\n", sizeof(Thread));
printf("sizeof(Worker_Info): %zd\n", sizeof(Worker_Info));

View File

@ -202,16 +202,14 @@ internal bool thread_init (Thread* thread, Thread_Proc proc, string thread_name=
s64 this_thread_index = InterlockedIncrement(&next_thread_index);
// We may not always want such a bulky thread startup. The
// size of the starting arena and temp should be parameterized (+2 bytes)
// make thread_init_ex with params...
Arena* arena = next_arena(Arena_Reserve::Size_64G);
push_arena(arena);
thread->context = New<Thread_Context>(get_allocator(arena));
thread->context->temp = next_arena(Arena_Reserve::Size_64G);
thread->context->arena = arena;
thread->context->allocator = get_allocator(arena);
ExpandableArena* arena_ex = expandable_arena_new(Arena_Reserve::Size_64M, 16);
thread->context = New<Thread_Context>(get_allocator(arena_ex));
thread->context->temp = expandable_arena_new(Arena_Reserve::Size_2M, 16);
thread->context->arena = arena_ex;
thread->context->allocator = get_allocator(arena_ex);
thread->context->thread_idx = (s32)this_thread_index;
push_arena(arena_ex);
thread->context->thread_name = copy_string(thread_name);
thread->os_thread.windows_thread = windows_thread;
@ -230,8 +228,8 @@ internal void thread_deinit (Thread* thread) {
thread->os_thread.windows_thread = nullptr;
// #TODO: Thread cleanup:
release_arena(thread->context->temp, true);
release_arena(thread->context->arena, true);
arena_delete(thread->context->temp);
arena_delete(thread->context->arena);
}
internal void thread_start (Thread* thread) {

View File

@ -9,16 +9,19 @@
#include "lib/Base/Base.h"
#include "lib/Base/Allocator.h"
#include "lib/Base/Array.h"
#include "lib/Base/String.cpp"
#include "lib/Base/General_Purpose_Allocator.h"
#include "lib/Base/Arena.h"
#include "lib/Base/Arena_Array.h"
#include "lib/Base/Arena_Table.cpp"
#include "lib/Base/Base_Thread_Context.h"
#include "lib/Base/Expandable_Arena.h"
#include "lib/Base/String.cpp"
#include "lib/Base/ErrorCodes.cpp"
#include "lib/Base/Arena.cpp"
#include "lib/Base/Arena_Table.cpp"
#include "lib/Base/Base_Thread_Context.cpp"
#include "lib/Base/Expandable_Arena.cpp"
#include "lib/Base/Allocator.cpp"
#include "lib/Base/General_Purpose_Allocator.cpp"
#include "lib/Base/Basic.cpp"