diff --git a/core/runtime/default_allocators_arena.odin b/core/runtime/default_allocators_arena.odin new file mode 100644 index 000000000..5309893fb --- /dev/null +++ b/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 +} diff --git a/core/runtime/default_temporary_allocator.odin b/core/runtime/default_temporary_allocator.odin index b71cd103a..cfe5cdd1a 100644 --- a/core/runtime/default_temporary_allocator.odin +++ b/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 { 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_proc :: nil_allocator_proc } else { 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) { - 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, @@ -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) { 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 { return Allocator{ procedure = default_temp_allocator_proc, - data = allocator, + data = allocator, } }