From 2a0543d2f01e2ae0e8ddab1f31bcf96f14887e08 Mon Sep 17 00:00:00 2001 From: gingerBill Date: Wed, 6 Mar 2024 16:14:45 +0000 Subject: [PATCH] Add `mem.Buddy_Allocator` --- core/mem/allocators.odin | 264 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) diff --git a/core/mem/allocators.odin b/core/mem/allocators.odin index d30dbcb0f..587c8db04 100644 --- a/core/mem/allocators.odin +++ b/core/mem/allocators.odin @@ -862,3 +862,267 @@ panic_allocator :: proc() -> Allocator { } + + + + +Buddy_Block :: struct #align(align_of(uint)) { + size: uint, + is_free: bool, +} + +@(require_results) +buddy_block_next :: proc(block: ^Buddy_Block) -> ^Buddy_Block { + return (^Buddy_Block)(([^]byte)(block)[block.size:]) +} + +@(require_results) +buddy_block_split :: proc(block: ^Buddy_Block, size: uint) -> ^Buddy_Block { + block := block + if block != nil && size != 0 { + // Recursive Split + for size < block.size { + sz := block.size >> 1 + block.size = sz + block = buddy_block_next(block) + block.size = sz + block.is_free = true + } + if size <= block.size { + return block + } + } + // Block cannot fit the requested allocation size + return nil +} + +buddy_block_coalescence :: proc(head, tail: ^Buddy_Block) { + for { + // Keep looping until there are no more buddies to coalesce + block := head + buddy := buddy_block_next(block) + + no_coalescence := true + for block < tail && buddy < tail { // make sure the buddies are within the range + if block.is_free && buddy.is_free && block.size == buddy.size { + // Coalesce buddies into one + block.size <<= 1 + block = buddy_block_next(block) + if block < tail { + buddy = buddy_block_next(block) + no_coalescence = false + } + } else if block.size < buddy.size { + // The buddy block is split into smaller blocks + block = buddy + buddy = buddy_block_next(buddy) + } else { + block = buddy_block_next(buddy) + if block < tail { + // Leave the buddy block for the next iteration + buddy = buddy_block_next(block) + } + } + } + + if no_coalescence { + return + } + } +} + + +@(require_results) +buddy_block_find_best :: proc(head, tail: ^Buddy_Block, size: uint) -> ^Buddy_Block { + assert(size != 0) + + best_block: ^Buddy_Block + block := head // left + buddy := buddy_block_next(block) // right + + // The entire memory section between head and tail is free, + // just call 'buddy_block_split' to get the allocation + if buddy == tail && block.is_free { + return buddy_block_split(block, size) + } + + // Find the block which is the 'best_block' to requested allocation sized + for block < tail && buddy < tail { // make sure the buddies are within the range + // If both buddies are free, coalesce them together + // NOTE: this is an optimization to reduce fragmentation + // this could be completely ignored + if block.is_free && buddy.is_free && block.size == buddy.size { + block.size <<= 1 + if size <= block.size && (best_block == nil || block.size <= best_block.size) { + best_block = block + } + + block = buddy_block_next(buddy) + if block < tail { + // Delay the buddy block for the next iteration + buddy = buddy_block_next(block) + } + continue + } + + + if block.is_free && size <= block.size && + (best_block == nil || block.size <= best_block.size) { + best_block = block + } + + if buddy.is_free && size <= buddy.size && + (best_block == nil || buddy.size < best_block.size) { + // If each buddy are the same size, then it makes more sense + // to pick the buddy as it "bounces around" less + best_block = buddy + } + + if (block.size <= buddy.size) { + block = buddy_block_next(buddy) + if (block < tail) { + // Delay the buddy block for the next iteration + buddy = buddy_block_next(block) + } + } else { + // Buddy was split into smaller blocks + block = buddy + buddy = buddy_block_next(buddy) + } + } + + if best_block != nil { + // This will handle the case if the 'best_block' is also the perfect fit + return buddy_block_split(best_block, size) + } + + // Maybe out of memory + return nil +} + + +Buddy_Allocator :: struct { + head: ^Buddy_Block, + tail: ^Buddy_Block, + alignment: uint, +} + +buddy_allocator :: proc(b: ^Buddy_Allocator) -> Allocator { + return Allocator{ + procedure = buddy_allocator_proc, + data = b, + } +} + +buddy_allocator_init :: proc(b: ^Buddy_Allocator, data: []byte, alignment: uint) { + assert(data != nil) + assert(is_power_of_two(uintptr(len(data)))) + assert(is_power_of_two(uintptr(alignment))) + + alignment := alignment + if alignment < size_of(Buddy_Block) { + alignment = size_of(Buddy_Block) + } + + ptr := raw_data(data) + assert(uintptr(ptr) % uintptr(alignment) == 0, "data is not aligned to minimum alignment") + + b.head = (^Buddy_Block)(ptr) + + b.head.size = len(data) + b.head.is_free = true + + b.tail = buddy_block_next(b.head) + + b.alignment = alignment +} + +@(require_results) +buddy_block_size_required :: proc(b: ^Buddy_Allocator, size: uint) -> uint { + size := size + actual_size := b.alignment + size += size_of(Buddy_Block) + size = align_forward_uint(size, b.alignment) + + for size > actual_size { + actual_size <<= 1 + } + + return actual_size +} + +@(require_results) +buddy_allocator_alloc :: proc(b: ^Buddy_Allocator, size: uint, zeroed: bool) -> ([]byte, Allocator_Error) { + if size != 0 { + actual_size := buddy_block_size_required(b, size) + + found := buddy_block_find_best(b.head, b.tail, actual_size) + if found != nil { + // Try to coalesce all the free buddy blocks and then search again + buddy_block_coalescence(b.head, b.tail) + found = buddy_block_find_best(b.head, b.tail, actual_size) + } + if found == nil { + return nil, .Out_Of_Memory + } + found.is_free = false + + data := ([^]byte)(found)[b.alignment:][:size] + if zeroed { + zero_slice(data) + } + return data, nil + } + return nil, nil +} + +buddy_allocator_free :: proc(b: ^Buddy_Allocator, ptr: rawptr) -> Allocator_Error { + if ptr != nil { + if !(b.head <= ptr && ptr <= b.tail) { + return .Invalid_Pointer + } + + block := (^Buddy_Block)(([^]byte)(ptr)[-b.alignment:]) + block.is_free = true + + buddy_block_coalescence(b.head, b.tail) + } + return nil +} + +buddy_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode, + size, alignment: int, + old_memory: rawptr, old_size: int,loc := #caller_location) -> ([]byte, Allocator_Error) { + + b := (^Buddy_Allocator)(allocator_data) + + switch mode { + case .Alloc, .Alloc_Non_Zeroed: + return buddy_allocator_alloc(b, uint(size), mode == .Alloc) + case .Resize: + return default_resize_bytes_align(byte_slice(old_memory, old_size), size, alignment, buddy_allocator(b)) + case .Resize_Non_Zeroed: + return default_resize_bytes_align_non_zeroed(byte_slice(old_memory, old_size), size, alignment, buddy_allocator(b)) + case .Free: + return nil, buddy_allocator_free(b, old_memory) + case .Free_All: + + alignment := b.alignment + head := ([^]byte)(b.head) + tail := ([^]byte)(b.tail) + data := head[:ptr_sub(tail, head)] + buddy_allocator_init(b, data, alignment) + + case .Query_Features: + set := (^Allocator_Mode_Set)(old_memory) + if set != nil { + set^ = {.Query_Features, .Alloc, .Alloc_Non_Zeroed, .Resize, .Resize_Non_Zeroed, .Free, .Free_All} + } + return nil, nil + + case .Query_Info: + return nil, .Mode_Not_Implemented + } + + return nil, nil +}