From 3ee5dd7d65224ec19d35cc87eb03ecb1a5534e33 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 5 Mar 2024 10:38:12 -0500 Subject: [PATCH] First complete iteration of stack allocator --- code/girme_stack.odin | 249 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 6 deletions(-) diff --git a/code/girme_stack.odin b/code/girme_stack.odin index eaaf282..670afc0 100644 --- a/code/girme_stack.odin +++ b/code/girme_stack.odin @@ -1,18 +1,23 @@ package sectr -Stack :: struct ( $ Type : typeid, $ Size : i32 ) { - idx : i32, +import "core:mem" +import "core:slice" + +//region Fixed Stack + +StackFixed :: struct ( $ Type : typeid, $ Size : u32 ) { + idx : u32, items : [ Size ] Type, } -stack_push :: proc( using stack : ^ Stack( $ Type, $ Size ), value : Type ) { +stack_push :: proc( using stack : ^ StackFixed( $ Type, $ Size ), value : Type ) { verify( idx < len( items ), "Attempted to push on a full stack" ) items[ idx ] = value idx += 1 } -stack_pop :: proc( using stack : ^ Stack( $ Type, $ Size ) ) { +stack_pop :: proc( using stack : ^StackFixed( $ Type, $ Size ) ) { verify( idx > 0, "Attempted to pop an empty stack" ) idx -= 1 @@ -21,12 +26,244 @@ stack_pop :: proc( using stack : ^ Stack( $ Type, $ Size ) ) { } } -stack_peek_ref :: proc( using stack : ^ Stack( $ Type, $ Size ) ) -> ^ Type { +stack_peek_ref :: proc( using stack : ^StackFixed( $ Type, $ Size ) ) -> ( ^Type) { last := max( 0, idx - 1 ) return & items[last] } -stack_peek :: proc ( using stack : ^ Stack( $ Type, $ Size ) ) -> Type { +stack_peek :: proc ( using stack : ^StackFixed( $ Type, $ Size ) ) -> Type { last := max( 0, idx - 1 ) return items[last] } + +//endregion Fixed Stack + +//region Stack Allocator + +/* Growing Stack allocator + This implementation can support growing if the backing allocator supports + it without fragmenting the back allocator. + + Each block in the stack is tracked with a doubly-linked list to have debug stats. + (It could be removed for non-debug builds) +*/ + +StackAllocatorBase :: struct { + backing : Allocator, + + using links : DLL_NodeFL(StackAllocatorHeader), + peak_used : int, + size : int, + data : [^]byte, +} + +StackAllocator :: struct { + using base : ^StackAllocatorBase, +} + +StackAllocatorHeader :: struct { + using links : DLL_NodePN(StackAllocatorHeader), + block_size : int, + padding : int, +} + +stack_allocator :: proc( using self : StackAllocator ) -> ( allocator : Allocator ) { + allocator.procedure = stack_allocator_proc + allocator.data = self.base + return +} + +stack_allocator_init :: proc( size : int, allocator := context.allocator ) -> ( stack : StackAllocator, alloc_error : AllocatorError ) +{ + header_size := size_of(StackAllocatorBase) + + raw_mem : rawptr + raw_mem, alloc_error = alloc( header_size + size, mem.DEFAULT_ALIGNMENT ) + if alloc_error != AllocatorError.None do return + + stack.base = cast( ^StackAllocatorBase) raw_mem + stack.size = size + stack.data = cast( [^]byte) (cast( [^]StackAllocatorBase) stack.base)[ 1:] + + stack.top = cast(^StackAllocatorHeader) stack.data + stack.bottom = stack.first + return +} + +stack_allocator_init_via_memory :: proc( memory : []byte ) -> ( stack : StackAllocator ) +{ + header_size := size_of(StackAllocatorBase) + + if len(memory) < (header_size + Kilobyte) { + verify(false, "Assigning a stack allocator less than a kilobyte of space") + return + } + + stack.base = cast( ^StackAllocatorBase) & memory[0] + stack.size = len(memory) - header_size + stack.data = cast( [^]byte ) (cast( [^]StackAllocatorBase) stack.base)[ 1:] + + stack.top = cast( ^StackAllocatorHeader) stack.data + stack.bottom = stack.first + return +} + +stack_allocator_push :: proc( using self : StackAllocator, block_size, alignment : int, zero_memory : bool ) -> ( []byte, AllocatorError ) +{ + // TODO(Ed): Make sure first push is fine. + verify( block_size > Kilobyte, "Attempted to push onto the stack less than a Kilobyte") + top_block_ptr := memory_after_header( top ) + + theoretical_size := cast(int) (uintptr(top_block_ptr) + uintptr(block_size) - uintptr(bottom)) + if theoretical_size > size { + // TODO(Ed) : Check if backing allocator supports resize, if it does attempt to grow. + return nil, .Out_Of_Memory + } + + top_block_slice := slice_ptr( top_block_ptr, top.block_size ) + next_spot := uintptr( top_block_ptr) + uintptr(top.block_size) + + header_offset_pad := calc_padding_with_header( uintptr(next_spot), uintptr(alignment), size_of(StackAllocatorHeader) ) + header := cast( ^StackAllocatorHeader) (next_spot + uintptr(header_offset_pad) - uintptr(size_of( StackAllocatorHeader))) + header.padding = header_offset_pad + header.prev = last + header.block_size = block_size + + curr_block_ptr := memory_after_header( header ) + curr_block := slice_ptr( curr_block_ptr, block_size ) + + curr_used := cast(int) (uintptr(curr_block_ptr) + uintptr(block_size) - uintptr(self.top)) + self.peak_used += max( peak_used, curr_used ) + + dll_push_back( & base.links.last, header ) + + if zero_memory { + slice.zero( curr_block ) + } + + return curr_block, .None +} + +stack_allocator_resize_top :: proc( using self : StackAllocator, new_block_size, alignment : int, zero_memory : bool ) -> AllocatorError +{ + verify( new_block_size > Kilobyte, "Attempted to resize the last pushed on the stack to less than a Kilobyte") + top_block_ptr := memory_after_header( top ) + + theoretical_size := cast(int) (uintptr(top_block_ptr) + uintptr(top.block_size) - uintptr(bottom)) + if theoretical_size > size { + // TODO(Ed) : Check if backing allocator supports resize, if it does attempt to grow. + return .Out_Of_Memory + } + + if zero_memory && new_block_size > top.block_size { + added_ptr := top_block_ptr[ top.block_size:] + added_slice := slice_ptr( added_ptr, new_block_size - top.block_size ) + slice.zero( added_slice ) + } + + top.block_size = new_block_size + return .None +} + +stack_allocator_pop :: proc( using self : StackAllocator ) { + base.links.top = top.prev + base.links.top.next = nil +} + +stack_allocator_proc :: proc( + allocator_data : rawptr, + mode : AllocatorMode, + block_size : int, + alignment : int, + old_memory : rawptr, + old_size : int, + location : SourceCodeLocation = #caller_location +) -> ([]byte, AllocatorError) +{ + stack := StackAllocator { cast( ^StackAllocatorBase) allocator_data } + + if stack.data == nil { + return nil, AllocatorError.Invalid_Argument + } + + switch mode + { + case .Alloc, .Alloc_Non_Zeroed: + { + return stack_allocator_push( stack, block_size, alignment, mode == .Alloc ) + } + case .Free: + { + if old_memory == nil { + return nil, .None + } + + start := uintptr(stack.data) + end := start + uintptr(block_size) + curr_addr := uintptr(old_memory) + + verify( start <= curr_addr && curr_addr < end, "Out of bounds memory address passed to stack allocator (free)" ) + + block_ptr := memory_after_header( stack.last ) + + if curr_addr >= start + uintptr(block_ptr) { + return nil, .None + } + + dll_pop_back( & stack.last, stack.last ) + } + case .Free_All: + { + // TODO(Ed) : Review that we don't have any header issues with the reset. + stack.last = stack.first + stack.first.next = nil + stack.first.block_size = 0 + } + case .Resize, .Resize_Non_Zeroed: + { + // Check if old_memory is at the first on the stack, if it is, just grow its size + // Otherwise, log that the user cannot resize stack items that are not at the top of the stack allocated. + if old_memory == nil { + return stack_allocator_push(stack, block_size, alignment, mode == .Resize ) + } + if block_size == 0 { + return nil, .None + } + + start := uintptr(stack.data) + end := start + uintptr(block_size) + curr_addr := uintptr(old_memory) + + verify( start <= curr_addr && curr_addr < end, "Out of bounds memory address passed to stack allocator (resize)" ) + + block_ptr := memory_after_header( stack.top ) + if block_ptr != old_memory { + ensure( false, "Attempted to reszie a block of memory on the stack other than top most" ) + return nil, .None + } + + if old_size == block_size { + return byte_slice( old_memory, block_size ), .None + } + + stack_allocator_resize_top( stack, block_size, alignment, mode == .Resize ) + return byte_slice( block_ptr, block_size ), .None + } + case .Query_Features: + { + feature_flags := ( ^AllocatorModeSet)(old_memory) + if feature_flags != nil { + (feature_flags ^) = {.Alloc, .Alloc_Non_Zeroed, .Free, .Free_All, .Resize, .Resize_Non_Zeroed, .Query_Features} + } + return nil, .None + } + case .Query_Info: + { + return nil, .Mode_Not_Implemented + } + } + + return nil, .None +} + +//endregion Stack Allocator