Browse Source

Add `mem.Buddy_Allocator`

gingerBill 1 year ago
parent
commit
2a0543d2f0
1 changed files with 264 additions and 0 deletions
  1. 264 0
      core/mem/allocators.odin

+ 264 - 0
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
+}