Browse Source

Replace current default `context.temp_allocator` to use a growing arena rather than a ring buffer

gingerBill 2 years ago
parent
commit
8626f58773
2 changed files with 237 additions and 165 deletions
  1. 228 0
      core/runtime/default_allocators_arena.odin
  2. 9 165
      core/runtime/default_temporary_allocator.odin

+ 228 - 0
core/runtime/default_allocators_arena.odin

@@ -0,0 +1,228 @@
+package runtime
+
+import "core:intrinsics"
+
+DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE :: uint(DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE)
+
+Memory_Block :: struct {
+	prev:      ^Memory_Block,
+	allocator: Allocator,
+	base:      [^]byte,
+	used:      uint,
+	capacity:  uint,
+}
+
+Arena :: struct {
+	backing_allocator:  Allocator,
+	curr_block:         ^Memory_Block,
+	total_used:         uint,
+	total_capacity:     uint,
+	minimum_block_size: uint,
+}
+
+@(private, require_results)
+safe_add :: #force_inline proc "contextless" (x, y: uint) -> (uint, bool) {
+	z, did_overflow := intrinsics.overflow_add(x, y)
+	return z, !did_overflow
+}
+
+@(require_results)
+memory_block_alloc :: proc(allocator: Allocator, capacity: uint, loc := #caller_location) -> (block: ^Memory_Block, err: Allocator_Error) {
+	total_size := uint(capacity + size_of(Memory_Block))
+	base_offset    := uintptr(size_of(Memory_Block))
+	protect_offset := uintptr(0)
+
+	min_alignment: int = max(16, align_of(Memory_Block))
+	data := mem_alloc(int(total_size), min_alignment, allocator, loc) or_return
+	block = (^Memory_Block)(raw_data(data))
+	end := uintptr(raw_data(data)[len(data):])
+
+	block.allocator = allocator
+	block.base = ([^]byte)(uintptr(block) + base_offset)
+	block.capacity = uint(end - uintptr(block.base))
+
+	// Should be zeroed
+	assert(block.used == 0)
+	assert(block.prev == nil)
+	return
+}
+
+memory_block_dealloc :: proc(block_to_free: ^Memory_Block, loc := #caller_location) {
+	if block_to_free != nil {
+		allocator := block_to_free.allocator
+		mem_free(block_to_free, allocator, loc)
+	}
+}
+
+@(require_results)
+alloc_from_memory_block :: proc(block: ^Memory_Block, min_size, alignment: uint) -> (data: []byte, err: Allocator_Error) {
+	calc_alignment_offset :: proc "contextless" (block: ^Memory_Block, alignment: uintptr) -> uint {
+		alignment_offset := uint(0)
+		ptr := uintptr(block.base[block.used:])
+		mask := alignment-1
+		if ptr & mask != 0 {
+			alignment_offset = uint(alignment - (ptr & mask))
+		}
+		return alignment_offset
+
+	}
+	if block == nil {
+		return nil, .Out_Of_Memory
+	}
+	alignment_offset := calc_alignment_offset(block, uintptr(alignment))
+	size, size_ok := safe_add(min_size, alignment_offset)
+	if !size_ok {
+		err = .Out_Of_Memory
+		return
+	}
+
+	if to_be_used, ok := safe_add(block.used, size); !ok || to_be_used > block.capacity {
+		err = .Out_Of_Memory
+		return
+	}
+	data = block.base[block.used+alignment_offset:][:min_size]
+	block.used += size
+	return
+}
+
+@(require_results)
+arena_alloc :: proc(arena: ^Arena, size, alignment: uint, loc := #caller_location) -> (data: []byte, err: Allocator_Error) {
+	align_forward_uint :: proc "contextless" (ptr, align: uint) -> uint {
+		p := ptr
+		modulo := p & (align-1)
+		if modulo != 0 {
+			p += align - modulo
+		}
+		return p
+	}
+
+	assert(alignment & (alignment-1) == 0, "non-power of two alignment", loc)
+
+	size := size
+	if size == 0 {
+		return
+	}
+
+	if arena.curr_block == nil || (safe_add(arena.curr_block.used, size) or_else 0) > arena.curr_block.capacity {
+		size = align_forward_uint(size, alignment)
+		if arena.minimum_block_size == 0 {
+			arena.minimum_block_size = DEFAULT_ARENA_GROWING_MINIMUM_BLOCK_SIZE
+		}
+
+		block_size := max(size, arena.minimum_block_size)
+
+		if arena.backing_allocator.procedure == nil {
+			arena.backing_allocator = default_allocator()
+		}
+
+		new_block := memory_block_alloc(arena.backing_allocator, block_size, loc) or_return
+		new_block.prev = arena.curr_block
+		arena.curr_block = new_block
+		arena.total_capacity += new_block.capacity
+	}
+
+	prev_used := arena.curr_block.used
+	data, err = alloc_from_memory_block(arena.curr_block, size, alignment)
+	arena.total_used += arena.curr_block.used - prev_used
+	return
+}
+
+// `arena_init` will initialize the arena with a usuable block.
+// This procedure is not necessary to use the Arena as the default zero as `arena_alloc` will set things up if necessary
+@(require_results)
+arena_init :: proc(arena: ^Arena, size: uint, backing_allocator: Allocator, loc := #caller_location) -> Allocator_Error {
+	arena^ = {}
+	arena.backing_allocator = backing_allocator
+	arena.minimum_block_size = max(size, 1<<12) // minimum block size of 4 KiB
+	new_block := memory_block_alloc(arena.backing_allocator, arena.minimum_block_size, loc) or_return
+	arena.curr_block = new_block
+	arena.total_capacity += new_block.capacity
+	return nil
+}
+
+
+// `arena_free_all` will free all but the first memory block, and then reset the memory block
+arena_free_all :: proc(arena: ^Arena, loc := #caller_location) {
+	for arena.curr_block != nil && arena.curr_block.prev != nil {
+		free_block := arena.curr_block
+		arena.curr_block = free_block.prev
+
+		arena.total_capacity -= free_block.capacity
+		memory_block_dealloc(free_block, loc)
+	}
+
+	if arena.curr_block != nil {
+		intrinsics.mem_zero(arena.curr_block.base, arena.curr_block.used)
+		arena.curr_block.used = 0
+	}
+	arena.total_used = 0
+}
+
+arena_destroy :: proc(arena: ^Arena, loc := #caller_location) {
+	for arena.curr_block != nil {
+		free_block := arena.curr_block
+		arena.curr_block = free_block.prev
+
+		arena.total_capacity -= free_block.capacity
+		memory_block_dealloc(free_block, loc)
+	}
+	arena.total_used = 0
+	arena.total_capacity = 0
+}
+
+arena_allocator :: proc(arena: ^Arena) -> Allocator {
+	return Allocator{arena_allocator_proc, arena}
+}
+
+arena_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode,
+                             size, alignment: int,
+                             old_memory: rawptr, old_size: int,
+                             location := #caller_location) -> (data: []byte, err: Allocator_Error) {
+	arena := (^Arena)(allocator_data)
+
+	size, alignment := uint(size), uint(alignment)
+	old_size := uint(old_size)
+
+	switch mode {
+	case .Alloc, .Alloc_Non_Zeroed:
+		return arena_alloc(arena, size, alignment, location)
+	case .Free:
+		err = .Mode_Not_Implemented
+	case .Free_All:
+		arena_free_all(arena, location)
+	case .Resize:
+		old_data := ([^]byte)(old_memory)
+
+		switch {
+		case old_data == nil:
+			return arena_alloc(arena, size, alignment, location)
+		case size == old_size:
+			// return old memory
+			data = old_data[:size]
+			return
+		case size == 0:
+			err = .Mode_Not_Implemented
+			return
+		case (uintptr(old_data) & uintptr(alignment-1) == 0) && size < old_size:
+			// shrink data in-place
+			data = old_data[:size]
+			return
+		}
+
+		new_memory := arena_alloc(arena, size, alignment, location) or_return
+		if new_memory == nil {
+			return
+		}
+		copy(new_memory, old_data[:old_size])
+		return new_memory, nil
+	case .Query_Features:
+		set := (^Allocator_Mode_Set)(old_memory)
+		if set != nil {
+			set^ = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features}
+		}
+	case .Query_Info:
+		err = .Mode_Not_Implemented
+	}
+
+	return
+}

+ 9 - 165
core/runtime/default_temporary_allocator.odin

@@ -6,154 +6,25 @@ DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE: int : #config(DEFAULT_TEMP_ALLOCATOR_BACKIN
 when ODIN_OS == .Freestanding || ODIN_OS == .JS || ODIN_DEFAULT_TO_NIL_ALLOCATOR {
 when ODIN_OS == .Freestanding || ODIN_OS == .JS || ODIN_DEFAULT_TO_NIL_ALLOCATOR {
 	Default_Temp_Allocator :: struct {}
 	Default_Temp_Allocator :: struct {}
 	
 	
-	default_temp_allocator_init :: proc(s: ^Default_Temp_Allocator, size: int, backup_allocator := context.allocator) {}
+	default_temp_allocator_init :: proc(s: ^Default_Temp_Allocator, size: int, backing_allocator := context.allocator) {}
 	
 	
 	default_temp_allocator_destroy :: proc(s: ^Default_Temp_Allocator) {}
 	default_temp_allocator_destroy :: proc(s: ^Default_Temp_Allocator) {}
 	
 	
 	default_temp_allocator_proc :: nil_allocator_proc
 	default_temp_allocator_proc :: nil_allocator_proc
 } else {
 } else {
 	Default_Temp_Allocator :: struct {
 	Default_Temp_Allocator :: struct {
-		data:               []byte,
-		curr_offset:        int,
-		prev_allocation:    rawptr,
-		backup_allocator:   Allocator,
-		leaked_allocations: [dynamic][]byte,
+		arena: Arena,
 	}
 	}
 	
 	
-	default_temp_allocator_init :: proc(s: ^Default_Temp_Allocator, size: int, backup_allocator := context.allocator) {
-		s.data = make_aligned([]byte, size, 2*align_of(rawptr), backup_allocator)
-		s.curr_offset = 0
-		s.prev_allocation = nil
-		s.backup_allocator = backup_allocator
-		s.leaked_allocations.allocator = backup_allocator
+	default_temp_allocator_init :: proc(s: ^Default_Temp_Allocator, size: int, backing_allocator := context.allocator) {
+		_ = arena_init(&s.arena, uint(size), backing_allocator)
 	}
 	}
 
 
 	default_temp_allocator_destroy :: proc(s: ^Default_Temp_Allocator) {
 	default_temp_allocator_destroy :: proc(s: ^Default_Temp_Allocator) {
-		if s == nil {
-			return
+		if s != nil {
+			arena_destroy(&s.arena)
+			s^ = {}
 		}
 		}
-		for ptr in s.leaked_allocations {
-			free(raw_data(ptr), s.backup_allocator)
-		}
-		delete(s.leaked_allocations)
-		delete(s.data, s.backup_allocator)
-		s^ = {}
-	}
-
-	@(private)
-	default_temp_allocator_alloc :: proc(s: ^Default_Temp_Allocator, size, alignment: int, loc := #caller_location) -> ([]byte, Allocator_Error) {
-		size := size
-		size = align_forward_int(size, alignment)
-
-		switch {
-		case s.curr_offset+size <= len(s.data):
-			start := uintptr(raw_data(s.data))
-			ptr := start + uintptr(s.curr_offset)
-			ptr = align_forward_uintptr(ptr, uintptr(alignment))
-			mem_zero(rawptr(ptr), size)
-
-			s.prev_allocation = rawptr(ptr)
-			offset := int(ptr - start)
-			s.curr_offset = offset + size
-			return byte_slice(rawptr(ptr), size), .None
-
-		case size <= len(s.data):
-			start := uintptr(raw_data(s.data))
-			ptr := align_forward_uintptr(start, uintptr(alignment))
-			mem_zero(rawptr(ptr), size)
-
-			s.prev_allocation = rawptr(ptr)
-			offset := int(ptr - start)
-			s.curr_offset = offset + size
-			return byte_slice(rawptr(ptr), size), .None
-		}
-		a := s.backup_allocator
-		if a.procedure == nil {
-			a = context.allocator
-			s.backup_allocator = a
-		}
-
-		data, err := mem_alloc_bytes(size, alignment, a, loc)
-		if err != nil {
-			return data, err
-		}
-		if s.leaked_allocations == nil {
-			s.leaked_allocations = make([dynamic][]byte, a)
-		}
-		append(&s.leaked_allocations, data)
-
-		// TODO(bill): Should leaks be notified about?
-		if logger := context.logger; logger.lowest_level <= .Warning {
-			if logger.procedure != nil {
-				logger.procedure(logger.data, .Warning, "default temp allocator resorted to backup_allocator" , logger.options, loc)
-			}
-		}
-
-		return data, .None
-	}
-
-	@(private)
-	default_temp_allocator_free :: proc(s: ^Default_Temp_Allocator, old_memory: rawptr, loc := #caller_location) -> Allocator_Error {
-		if old_memory == nil {
-			return .None
-		}
-
-		start := uintptr(raw_data(s.data))
-		end := start + uintptr(len(s.data))
-		old_ptr := uintptr(old_memory)
-
-		if s.prev_allocation == old_memory {
-			s.curr_offset = int(uintptr(s.prev_allocation) - start)
-			s.prev_allocation = nil
-			return .None
-		}
-
-		if start <= old_ptr && old_ptr < end {
-			// NOTE(bill): Cannot free this pointer but it is valid
-			return .None
-		}
-
-		if len(s.leaked_allocations) != 0 {
-			for data, i in s.leaked_allocations {
-				ptr := raw_data(data)
-				if ptr == old_memory {
-					free(ptr, s.backup_allocator)
-					ordered_remove(&s.leaked_allocations, i)
-					return .None
-				}
-			}
-		}
-		return .Invalid_Pointer
-		// panic("invalid pointer passed to default_temp_allocator");
-	}
-
-	@(private)
-	default_temp_allocator_free_all :: proc(s: ^Default_Temp_Allocator, loc := #caller_location) {
-		s.curr_offset = 0
-		s.prev_allocation = nil
-		for data in s.leaked_allocations {
-			free(raw_data(data), s.backup_allocator)
-		}
-		clear(&s.leaked_allocations)
-	}
-
-	@(private)
-	default_temp_allocator_resize :: proc(s: ^Default_Temp_Allocator, old_memory: rawptr, old_size, size, alignment: int, loc := #caller_location) -> ([]byte, Allocator_Error) {
-		begin := uintptr(raw_data(s.data))
-		end := begin + uintptr(len(s.data))
-		old_ptr := uintptr(old_memory)
-		if old_memory == s.prev_allocation && old_ptr & uintptr(alignment)-1 == 0 {
-			if old_ptr+uintptr(size) < end {
-				s.curr_offset = int(old_ptr-begin)+size
-				return byte_slice(old_memory, size), .None
-			}
-		}
-		data, err := default_temp_allocator_alloc(s, size, alignment, loc)
-		if err == .None {
-			copy(data, byte_slice(old_memory, old_size))
-			err = default_temp_allocator_free(s, old_memory, loc)
-		}
-		return data, err
 	}
 	}
 
 
 	default_temp_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode,
 	default_temp_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode,
@@ -161,40 +32,13 @@ when ODIN_OS == .Freestanding || ODIN_OS == .JS || ODIN_DEFAULT_TO_NIL_ALLOCATOR
 	                                    old_memory: rawptr, old_size: int, loc := #caller_location) -> (data: []byte, err: Allocator_Error) {
 	                                    old_memory: rawptr, old_size: int, loc := #caller_location) -> (data: []byte, err: Allocator_Error) {
 
 
 		s := (^Default_Temp_Allocator)(allocator_data)
 		s := (^Default_Temp_Allocator)(allocator_data)
-
-		if s.data == nil {
-			default_temp_allocator_init(s, DEFAULT_TEMP_ALLOCATOR_BACKING_SIZE, default_allocator())
-		}
-
-		switch mode {
-		case .Alloc, .Alloc_Non_Zeroed:
-			data, err = default_temp_allocator_alloc(s, size, alignment, loc)
-		case .Free:
-			err = default_temp_allocator_free(s, old_memory, loc)
-
-		case .Free_All:
-			default_temp_allocator_free_all(s, loc)
-
-		case .Resize:
-			data, err = default_temp_allocator_resize(s, old_memory, old_size, size, alignment, loc)
-
-		case .Query_Features:
-			set := (^Allocator_Mode_Set)(old_memory)
-			if set != nil {
-				set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Free_All, .Resize, .Query_Features}
-			}
-
-		case .Query_Info:
-			return nil, .Mode_Not_Implemented
-		}
-
-		return
+		return arena_allocator_proc(&s.arena, mode, size, alignment, old_memory, old_size, loc)
 	}
 	}
 }
 }
 
 
 default_temp_allocator :: proc(allocator: ^Default_Temp_Allocator) -> Allocator {
 default_temp_allocator :: proc(allocator: ^Default_Temp_Allocator) -> Allocator {
 	return Allocator{
 	return Allocator{
 		procedure = default_temp_allocator_proc,
 		procedure = default_temp_allocator_proc,
-		data = allocator,
+		data      = allocator,
 	}
 	}
 }
 }