#pragma once #include "base.h" /* =============================================================================== ARENA USAGE GUIDE =============================================================================== OVERVIEW -------- The arena allocator is a high-performance memory allocator designed for predictable allocation patterns. It supports: - Multiple chained memory blocks - Pointer-style allocation with free-list reuse - Stack-style allocation - Optional Swap-back removal for unordered data - Merging compatible arenas Memory is committed in page-sized chunks. The arena never commits less than the system page size and grows by allocating new blocks when needed. CORE CONCEPTS ------------- Arena Blocks ~~~~~~~~~~~~ An arena consists of one or more memory blocks linked together. Each block contains: - base_pos : The local starting position of a block (bytes) - pos : Global position (bytes) - commit_size : Total committed size (bytes) - reserve_size : Total usable size of the block (bytes) - prev/next: Links to neighboring blocks - the arena pointer points to the current block The arena allocates from the current block where the global position is or from the free list. Blocks form a single logical address space: global_offset = pos Global Position Model ~~~~~~~~~~~~~~~~~~~~~ All allocations are addressed using a global offset. This allows: - Popping across block boundaries - Cross-block swap-back operations - Arena merging by linking blocks ARENA CONFIGURATION ------------------- An arena is configured usings the struct arena_params before creation. Important parameters: - push_size : Fixed element size in bytes (0 = variable-size arena) - allow_free_list : Enables arena_free() and pointer mode, disabling it will enable stack mode - allow_swapback : Enables arena_swapback_pop(), works in stack mode only - max_nbre_blocks : Maximum number of blocks (0 = unlimited) - growth_policy : How blocks grow (next created block will have the same size or double the size) - commit_policy : How memory is committed (lazily if needed or commit all at block creation) ARENA CREATION AND DESTRUCTION ------------------------------ We create arenas using mem_arena *arena_create(arena_params *params); Behavior: - Create an arena and return the pointer to this arena or NULL if it fails Requirements: - The configuration of the arena needs to be injected as an argument using arena_params We destroy arenas using void *arena_destroy(mem_arena **arena_ptr); Behavior: - Return (void *)1 if it succeeds or NULL if it fails ALLOCATION (arena_push) ----------------------- We allocate using one function: void *arena_push(mem_arena **arena_ptr, u64 size, bool non_zero); Behavior: - zero can be set to true of false to zero the allocated memory or not - If the current block is full and block_index < max_nbre_blocks, Grows into a new block, setting the new block to arena->next then setting the arena pointer to the new block - The size can be set only if the arena has variable-size elements (push_size == 0) - Return the pointer of the pushed element if it succeeds or NULL if it fails Variable-size allocation (allow_free_list == true, pointer mode only): - Allocates 'size' bytes - Only available in pointer mode with a free list - Since the size of the elements is variable, when using the free list we loop through all the elements until finding one with enough size, after allocation if there is a remaining we store it as a new entry in the free list. The allocation is slower than fixed-size arenas - If there is not enough memory to push an element we try to create a new block and push the element to it, if there is some memory remaining in the previous block we add it to the free list - max_nbre_blocks determines the behavior of the free list - If max_nbre_blocks > 0 we push directly to the arena until it's full then we use the free list - If max_nbre_blocks = 0 (unlimited number of blocks) we use the free list directly Fixed-size allocation (allow_free_list can be true of false): - Size is ignored, push_size defines element size (arena config) - Available for both in stack and pointer modes - Caller-provided size is ignored - Required for swap-back correctness - Faster and safer than variable-size mode - If allow_free_list == true (pointer mode), uses the free list first then push DEALLOCATION ------------ POINTER MODE (WITH A FREE LIST) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Popping requires the pointer that points to the element - The only function allowed to pop is arena_free() void *arena_free(mem_arena **arena_ptr, u8 **ptr, u64 size); Behavior: - Stores (offset, size) in the free list - Memory is reused on future allocations - Sets the pointer to NULL - The freed memory is not zeroed, we can zero the memory only when allocating - If the element is last, behaves like a pop, decrementing pos, if the position crosses to the previous arena block, set the arena pointer to arena->prev - Return (void *)1 if it succeeds or NULL if it fails Requirements: - allow_free_list == true - Correct element size for variable-size arenas STACK MODE (HAS NO FREE LIST) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - If allow_swapback = false, we can only pop with arena_pop_to() - If allow_swapback = true, we can pop with arena_pop_to() and arena_swapback_pop() Pop to position: void *arena_pop_to(mem_arena **arena_ptr, u64 count); - Removes x elements from the top of the stack - Can remove data across blocks - Pops the element by moving the position (pos) - Does not zero memory we can zero the memory only when allocating - If we pop to the previous arena block, set the arena pointer to arena->prev - Return (void *)1 if it succeeds or NULL if it fails Requirements: - allow_free_list == false - Fixed-size elements (push_size > 0) - The number of elements to pop A macro arena_pop(arena) is available to pop one element void *arena_swapback_pop(mem_arena **arena_ptr, u64 index); - The top of the stack element is copied into the removed slot, the top of the stack is then reduced by one element - Works across multiple blocks - Return (void *)1 if it succeeds or NULL if it fails - Swapping back the last element will only pop it Cases: - Middle of block -> swapback - End of non-last block -> swapback - Last element overall -> pop only Requirements: - allow_free_list == false - Fixed-size elements (push_size > 0) - allow_swapback == true CLEARING THE ARENA ------------------ void *arena_clear(mem_arena **arena_ptr); - Resets pos for all blocks - Keeps all committed memory - Does NOT zero memory - Resets the arena pointer to the first arena block - Resets the free-list - Return (void *)1 if it succeeds or NULL if it fails MERGING ARENAS -------------- mem_arena *arena_merge(mem_arena **dst_ptr, mem_arena **src_ptr); Behavior: Merges arena B into arena A by: - Updating the pos and base_pos and block_index of all the blocks of B - Linking the two arenas by setting arena->prev of the first block of B to the last block of A - If there is empty blocks in arena A, append them to the end of arena B - Resets the arena pointer to arena B - Merges the free list arenas if available - max_nbre_blocks is summed - Return the pointer to the new arena of NULL if it fails Requirements: - Configurations must match exactly HELPER FUNCTIONS ------------------ mem_arena *arena_block_from_pos(mem_arena *last, u64 global_pos); mem_arena *arena_block_from_index(mem_arena *last, u64 index); mem_arena *arena_block_from_ptr(mem_arena *last, u8 *ptr); u64 arena_pos_from_ptr(mem_arena *arena, void *ptr); void *arena_ptr_from_pos(mem_arena *arena, u64 global_pos); void *arena_ptr_from_index(mem_arena *arena, u64 index); =============================================================================== */ #define ARENA_HEADER_SIZE (sizeof(mem_arena)) #define ARENA_ALIGN (sizeof(void *)) // arena config typedef enum arena_growth_policy { ARENA_GROWTH_NORMAL = 0, // grow by fixed block size ARENA_GROWTH_DOUBLE // double block size on each growth } arena_growth_policy; typedef enum arena_commit_policy { ARENA_COMMIT_LAZY = 0, // commit pages on demand ARENA_COMMIT_FULL // commit entire reserve at creation } arena_commit_policy; typedef struct arena_params { u64 reserve_size; // size of one arena block u64 commit_size; // initial commit size u64 align; // allocation alignment, 0 to disable and ARENA_ALIGN to align // according to architecture // Element size rules: // - stack mode : push_size > 0 (mandatory) // - pointer fixed : push_size > 0 // - pointer variable : push_size == 0 u64 push_size; struct mem_arena *free_list; b32 allow_free_list; // pointer mode if true b32 allow_swapback; // stack mode only arena_growth_policy growth_policy; arena_commit_policy commit_policy; u32 max_nbre_blocks; // 0 = unlimited } arena_params; typedef struct arena_free_node { u64 offset; // offset from arena base u64 size; // size of freed block } arena_free_node; // arena definition struct typedef struct mem_arena { // block chaining struct mem_arena *prev; struct mem_arena *next; // valid only on root arena // positions u64 base_pos; // of selected block u64 pos; // global pos // memory limits u64 reserve_size; u64 commit_size; u64 commit_pos; // configuration u64 align; u64 push_size; // Pointer mode only struct mem_arena *free_list; b32 allow_free_list; // Stack mode only b32 allow_swapback; arena_growth_policy growth_policy; arena_commit_policy commit_policy; u32 max_nbre_blocks; u32 block_index; } mem_arena; typedef struct mem_arena_temp { mem_arena *arena; u64 pos; } mem_arena_temp; // helper functions mem_arena *arena_block_from_pos(mem_arena *last, u64 global_pos); mem_arena *arena_block_from_index(mem_arena *last, u64 index); mem_arena *arena_block_from_ptr(mem_arena *last, u8 *ptr); u64 arena_pos_from_ptr(mem_arena *arena, void *ptr); void *arena_ptr_from_pos(mem_arena *arena, u64 global_pos); void *arena_ptr_from_index(mem_arena *arena, u64 index); // arena core functions // creation / destruction mem_arena *arena_create(arena_params *params); void *arena_destroy(mem_arena **arena_ptr); // allocation void *arena_push(mem_arena **arena_ptr, u64 size, bool non_zero); // pointer mode only // - fixed-size arena : size is ignored // - variable-size arena: size is mandatory void *arena_free(mem_arena **arena_ptr, u8 **ptr, u64 size); // stack mode only void *arena_pop_to(mem_arena **arena_ptr, u64 count); void *arena_swapback_pop(mem_arena **arena_ptr, u64 index); // utilities void *arena_clear(mem_arena **arena_ptr); mem_arena *arena_merge(mem_arena **dst_ptr, mem_arena **src_ptr); // temp arenas /* create a temporary arena on top of an existing arena, it: * takes an existing arena, marks it's position, allocate beyond this position * and free it until the marked position when not needed*/ mem_arena_temp arena_temp_begin(mem_arena *arena); void arena_temp_end(mem_arena_temp temp); mem_arena_temp arena_scratch_get(mem_arena **conflicts, u32 num_conflicts); void arena_scratch_release(mem_arena_temp scratch); #if defined(_MSC_VER) #define THREAD_LOCAL __declspec(thread) #else #define THREAD_LOCAL __thread #endif // Helpers // Pointer mode only /* Fixed-size arena: size is implicit, can pass 0 */ #define PUSH_STRUCT(arena, T) \ ((T *)arena_push((arena), 0, true)) // Zeroes the memory #define PUSH_STRUCT_NZ(arena, T) ((T *)arena_push((arena), 0, false)) #define PUSH_ARRAY(arena, T, n) ((T *)arena_push((arena), 0, true)) #define PUSH_ARRAY_NZ(arena, T, n) ((T *)arena_push((arena), 0, false)) /* Variable-size arena helpers: REQUIRE explicit size via arena_push() */ #define ARENA_PUSH(arena, size) arena_push((arena), (size), true) #define ARENA_PUSH_NZ(arena, size) arena_push((arena), (size), false) #define arena_pop(arena_ptr) arena_pop_to((arena_ptr), 1)