Add native heap allocator

- Add the test bench for the allocator
- Move old allocator code to the test bench
- Fix `heap_resize` usage in `os2/env_linux.odin` to fit new API
  requiring `old_size`
This commit is contained in:
Feoramund
2025-01-21 17:38:09 -05:00
parent f142043db9
commit a4a776634c
12 changed files with 3887 additions and 95 deletions

View File

@@ -9,111 +9,65 @@ heap_allocator :: proc() -> Allocator {
}
}
heap_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode,
size, alignment: int,
old_memory: rawptr, old_size: int, loc := #caller_location) -> ([]byte, Allocator_Error) {
//
// NOTE(tetra, 2020-01-14): The heap doesn't respect alignment.
// Instead, we overallocate by `alignment + size_of(rawptr) - 1`, and insert
// padding. We also store the original pointer returned by heap_alloc right before
// the pointer we return to the user.
//
aligned_alloc :: proc(size, alignment: int, old_ptr: rawptr, old_size: int, zero_memory := true) -> ([]byte, Allocator_Error) {
// Not(flysand): We need to reserve enough space for alignment, which
// includes the user data itself, the space to store the pointer to
// allocation start, as well as the padding required to align both
// the user data and the pointer.
a := max(alignment, align_of(rawptr))
space := a-1 + size_of(rawptr) + size
allocated_mem: rawptr
force_copy := old_ptr != nil && alignment > align_of(rawptr)
if old_ptr != nil && !force_copy {
original_old_ptr := ([^]rawptr)(old_ptr)[-1]
allocated_mem = heap_resize(original_old_ptr, space)
} else {
allocated_mem = heap_alloc(space, zero_memory)
}
aligned_mem := rawptr(([^]u8)(allocated_mem)[size_of(rawptr):])
ptr := uintptr(aligned_mem)
aligned_ptr := (ptr + uintptr(a)-1) & ~(uintptr(a)-1)
if allocated_mem == nil {
aligned_free(old_ptr)
aligned_free(allocated_mem)
heap_allocator_proc :: proc(
allocator_data: rawptr,
mode: Allocator_Mode,
size, alignment: int,
old_memory: rawptr,
old_size: int,
loc := #caller_location,
) -> ([]byte, Allocator_Error) {
assert(alignment <= HEAP_MAX_ALIGNMENT, "Heap allocation alignment beyond HEAP_MAX_ALIGNMENT bytes is not supported.", loc = loc)
assert(alignment >= 0, "Alignment must be greater than or equal to zero.", loc = loc)
switch mode {
case .Alloc:
// All allocations are aligned to at least their size up to
// `HEAP_MAX_ALIGNMENT`, and by virtue of binary arithmetic, any
// address aligned to N will also be aligned to N>>1.
//
// Therefore, we have no book-keeping costs for alignment.
ptr := heap_alloc(max(size, alignment))
if ptr == nil {
return nil, .Out_Of_Memory
}
aligned_mem = rawptr(aligned_ptr)
([^]rawptr)(aligned_mem)[-1] = allocated_mem
if force_copy {
mem_copy_non_overlapping(aligned_mem, old_ptr, min(old_size, size))
aligned_free(old_ptr)
return transmute([]byte)Raw_Slice{ data = ptr, len = size }, nil
case .Alloc_Non_Zeroed:
ptr := heap_alloc(max(size, alignment), zero = false)
if ptr == nil {
return nil, .Out_Of_Memory
}
return byte_slice(aligned_mem, size), nil
}
aligned_free :: proc(p: rawptr) {
if p != nil {
heap_free(([^]rawptr)(p)[-1])
return transmute([]byte)Raw_Slice{ data = ptr, len = size }, nil
case .Resize:
ptr := heap_resize(old_memory, old_size, max(size, alignment))
if ptr == nil {
return nil, .Out_Of_Memory
}
}
aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int, zero_memory := true) -> (new_memory: []byte, err: Allocator_Error) {
if p == nil {
return aligned_alloc(new_size, new_alignment, nil, old_size, zero_memory)
return transmute([]byte)Raw_Slice{ data = ptr, len = size }, nil
case .Resize_Non_Zeroed:
ptr := heap_resize(old_memory, old_size, max(size, alignment), zero = false)
if ptr == nil {
return nil, .Out_Of_Memory
}
new_memory = aligned_alloc(new_size, new_alignment, p, old_size, zero_memory) or_return
// NOTE: heap_resize does not zero the new memory, so we do it
if zero_memory && new_size > old_size {
new_region := raw_data(new_memory[old_size:])
intrinsics.mem_zero(new_region, new_size - old_size)
}
return
}
switch mode {
case .Alloc, .Alloc_Non_Zeroed:
return aligned_alloc(size, alignment, nil, 0, mode == .Alloc)
return transmute([]byte)Raw_Slice{ data = ptr, len = size }, nil
case .Free:
aligned_free(old_memory)
heap_free(old_memory)
case .Free_All:
return nil, .Mode_Not_Implemented
case .Resize, .Resize_Non_Zeroed:
return aligned_resize(old_memory, old_size, size, alignment, mode == .Resize)
case .Query_Features:
set := (^Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Resize, .Resize_Non_Zeroed, .Query_Features}
set^ = {
.Alloc,
.Alloc_Non_Zeroed,
.Resize,
.Resize_Non_Zeroed,
.Free,
.Query_Features,
}
}
return nil, nil
case .Query_Info:
return nil, .Mode_Not_Implemented
}
return nil, nil
}
heap_alloc :: proc "contextless" (size: int, zero_memory := true) -> rawptr {
return _heap_alloc(size, zero_memory)
}
heap_resize :: proc "contextless" (ptr: rawptr, new_size: int) -> rawptr {
return _heap_resize(ptr, new_size)
}
heap_free :: proc "contextless" (ptr: rawptr) {
_heap_free(ptr)
}

View File

@@ -0,0 +1,89 @@
package runtime
import "base:intrinsics"
/*
Merge all remote frees then free as many slabs as possible.
This bypasses any heuristics that keep slabs setup.
Returns true if the superpage was emptied and freed.
*/
@(private)
compact_superpage :: proc "contextless" (superpage: ^Heap_Superpage) -> (freed: bool) {
for i := 0; i < HEAP_SLAB_COUNT; /**/ {
slab := heap_superpage_index_slab(superpage, i)
if slab.bin_size > HEAP_MAX_BIN_SIZE {
// Skip contiguous slabs.
i += heap_slabs_needed_for_size(slab.bin_size)
} else if slab.bin_size > 0 {
i += 1
} else {
i += 1
continue
}
slab_is_cached := slab.free_bins > 0
heap_merge_remote_frees(slab)
if slab.free_bins == slab.max_bins {
if slab.bin_size > HEAP_MAX_BIN_SIZE {
heap_free_wide_slab(superpage, slab)
} else {
if slab_is_cached {
heap_cache_remove_slab(slab, heap_bin_size_to_rank(slab.bin_size))
}
heap_free_slab(superpage, slab)
}
}
}
if superpage.free_slabs == HEAP_SLAB_COUNT && !superpage.cache_block.in_use {
heap_free_superpage(superpage)
freed = true
}
return
}
/*
Merge all remote frees then free as many slabs and superpages as possible.
This bypasses any heuristics that keep slabs setup.
*/
compact_heap :: proc "contextless" () {
superpage := local_heap
for {
if superpage == nil {
return
}
next_superpage := superpage.next
compact_superpage(superpage)
superpage = next_superpage
}
}
/*
Free any empty superpages in the orphanage.
This procedure assumes there won't ever be more than 128 superpages in the
orphanage. This limitation is due to the avoidance of heap allocation.
*/
compact_heap_orphanage :: proc "contextless" () {
// First, try to empty the orphanage so that we can evaluate each superpage.
buffer: [128]^Heap_Superpage
for i := 0; i < len(buffer); i += 1 {
buffer[i] = heap_pop_orphan()
if buffer[i] == nil {
break
}
}
// Next, compact each superpage and push it back to the orphanage if it was
// not freed.
for superpage in buffer {
if !compact_superpage(superpage) {
heap_push_orphan(superpage)
}
}
}

View File

@@ -0,0 +1,93 @@
package runtime
import "base:intrinsics"
ODIN_DEBUG_HEAP :: #config(ODIN_DEBUG_HEAP, false)
Heap_Code_Coverage_Type :: enum {
Alloc_Bin,
Alloc_Collected_Remote_Frees,
Alloc_Heap_Initialized,
Alloc_Huge,
Alloc_Slab_Wide,
Alloc_Slab_Wide_Needed_New_Superpage,
Alloc_Slab_Wide_Used_Available_Superpage,
Alloc_Zeroed_Memory,
Freed_Bin_Freed_Slab_Which_Was_Fully_Used,
Freed_Bin_Freed_Superpage,
Freed_Bin_Reopened_Full_Slab,
Freed_Bin_Updated_Slab_Next_Free_Sector,
Freed_Huge_Allocation,
Freed_Wide_Slab,
Heap_Expanded_Cache_Data,
Huge_Alloc_Size_Adjusted,
Huge_Alloc_Size_Set_To_Superpage,
Merged_Remote_Frees,
Orphaned_Superpage_Freed_Slab,
Orphaned_Superpage_Merged_Remote_Frees,
Remotely_Freed_Bin,
Remotely_Freed_Bin_Caused_Remote_Superpage_Caching,
Remotely_Freed_Wide_Slab,
Resize_Caused_Memory_Zeroing,
Resize_Crossed_Size_Categories,
Resize_Huge,
Resize_Huge_Caused_Memory_Zeroing,
Resize_Huge_Size_Adjusted,
Resize_Huge_Size_Set_To_Superpage,
Resize_Kept_Old_Pointer,
Resize_Wide_Slab_Caused_Memory_Zeroing,
Resize_Wide_Slab_Expanded_In_Place,
Resize_Wide_Slab_Failed_To_Find_Contiguous_Expansion,
Resize_Wide_Slab_From_Remote_Thread,
Resize_Wide_Slab_Kept_Old_Pointer,
Resize_Wide_Slab_Shrunk_In_Place,
Slab_Adjusted_For_Partial_Sector,
Superpage_Add_Remote_Free_Guarded_With_Masterless,
Superpage_Add_Remote_Free_Guarded_With_Set,
Superpage_Added_Remote_Free,
Superpage_Added_To_Open_Cache_By_Freeing_Wide_Slab,
Superpage_Added_To_Open_Cache_By_Resizing_Wide_Slab,
Superpage_Added_To_Open_Cache_By_Slab,
Superpage_Adopted_From_Orphanage,
Superpage_Created_By_Empty_Orphanage,
Superpage_Freed_By_Exiting_Thread,
Superpage_Freed_By_Wide_Slab,
Superpage_Freed_On_Full_Orphanage,
Superpage_Linked,
Superpage_Cache_Block_Cleared,
Superpage_Orphaned_By_Exiting_Thread,
Superpage_Pushed_To_Orphanage,
Superpage_Registered_With_Free_Slabs,
Superpage_Registered_With_Slab_In_Use,
Superpage_Removed_From_Open_Cache_By_Slab,
Superpage_Unlinked_Non_Tail,
Superpage_Unlinked_Tail,
Superpage_Unregistered,
Superpage_Updated_Next_Free_Slab_Index,
Superpage_Updated_Next_Free_Slab_Index_As_Empty,
}
when ODIN_DEBUG_HEAP {
heap_global_code_coverage: [Heap_Code_Coverage_Type]int // atomic
}
@(private, disabled=!ODIN_DEBUG_HEAP)
heap_debug_cover :: #force_inline proc "contextless" (type: Heap_Code_Coverage_Type) {
when ODIN_DEBUG_HEAP {
intrinsics.atomic_add_explicit(&heap_global_code_coverage[type], 1, .Release)
}
}
_check_heap_code_coverage :: proc "contextless" () -> bool {
when ODIN_DEBUG_HEAP {
intrinsics.atomic_thread_fence(.Seq_Cst)
for t in heap_global_code_coverage {
if t == 0 {
return false
}
}
return true
} else {
panic_contextless("ODIN_DEBUG_HEAP is not enabled, therefore the results of this procedure are meaningless.")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
package runtime
import "base:intrinsics"
/*
Heap_Info provides metrics on a single thread's heap memory usage.
*/
Heap_Info :: struct {
total_memory_allocated_from_system: int `fmt:"M"`,
total_memory_used_for_book_keeping: int `fmt:"M"`,
total_memory_in_use: int `fmt:"M"`,
total_memory_free: int `fmt:"M"`,
total_memory_dirty: int `fmt:"M"`,
total_memory_remotely_free: int `fmt:"M"`,
total_superpages: int,
total_superpages_dedicated_to_heap_cache: int,
total_huge_allocations: int,
total_slabs: int,
total_slabs_in_use: int,
total_dirty_bins: int,
total_free_bins: int,
total_bins_in_use: int,
total_remote_free_bins: int,
heap_slab_map_entries: int,
heap_superpages_with_free_slabs: int,
heap_slabs_with_remote_frees: int,
}
/*
Get information about the current thread's heap.
This will do additional sanity checking on the heap if assertions are enabled.
*/
@(require_results)
get_local_heap_info :: proc "contextless" () -> (info: Heap_Info) {
if local_heap_cache != nil {
cache := local_heap_cache
slab_map_terminated: [HEAP_BIN_RANKS]bool
for {
for rank := 0; rank < HEAP_BIN_RANKS; rank += 1 {
for i := 0; i < HEAP_CACHE_SLAB_MAP_STRIDE; i += 1 {
slab := cache.slab_map[rank * HEAP_CACHE_SLAB_MAP_STRIDE + i]
if slab_map_terminated[rank] {
assert_contextless(slab == nil, "The heap allocator has a gap in its slab map.")
} else if slab == nil {
slab_map_terminated[rank] = true
} else {
info.heap_slab_map_entries += 1
assert_contextless(slab.bin_size != 0, "The heap allocator has an empty slab in its slab map.")
assert_contextless(slab.bin_size == 1 << (HEAP_MIN_BIN_SHIFT + uint(rank)), "The heap allocator has a slab in the wrong sub-array of the slab map.")
}
}
}
for superpage in cache.superpages_with_free_slabs {
if superpage != nil {
info.heap_superpages_with_free_slabs += 1
}
}
for i in 0..<len(cache.superpages_with_remote_frees) {
if intrinsics.atomic_load_explicit(&cache.superpages_with_remote_frees[i], .Seq_Cst) != nil {
info.heap_slabs_with_remote_frees += 1
}
}
if cache.next_cache_block == nil {
break
}
cache = cache.next_cache_block
}
}
superpage := local_heap
for {
if superpage == nil {
break
}
assert_contextless(superpage.owner == get_current_thread_id(), "The heap allocator for this thread has a superpage that belongs to another thread.")
info.total_superpages += 1
if superpage.huge_size > 0 {
info.total_huge_allocations += 1
info.total_memory_allocated_from_system += superpage.huge_size
info.total_memory_in_use += superpage.huge_size - HEAP_HUGE_ALLOCATION_BOOK_KEEPING
info.total_memory_used_for_book_keeping += HEAP_HUGE_ALLOCATION_BOOK_KEEPING
} else {
if superpage.cache_block.in_use {
info.total_superpages_dedicated_to_heap_cache += 1
}
info.total_memory_allocated_from_system += SUPERPAGE_SIZE
for i := 0; i < HEAP_SLAB_COUNT; /**/ {
slab := heap_superpage_index_slab(superpage, i)
if slab.bin_size != 0 {
info.total_slabs_in_use += 1
info.total_memory_in_use += slab.bin_size * (slab.max_bins - slab.free_bins)
info.total_memory_free += slab.bin_size * slab.free_bins
info.total_memory_dirty += slab.bin_size * slab.dirty_bins
info.total_bins_in_use += slab.max_bins - slab.free_bins
info.total_free_bins += slab.free_bins
info.total_dirty_bins += slab.dirty_bins
assert_contextless(slab.dirty_bins >= slab.max_bins - slab.free_bins, "A slab of the heap allocator has a number of dirty bins which is not equivalent to the number of its total bins minus the number of free bins.")
// Account for the bitmaps used by the Slab.
info.total_memory_used_for_book_keeping += int(slab.data - uintptr(slab))
// Account for the space not used by the bins or the bitmaps.
n := int(slab.data - uintptr(slab) + uintptr(slab.max_bins * slab.bin_size))
if slab.bin_size > HEAP_MAX_BIN_SIZE {
info.total_memory_used_for_book_keeping += heap_slabs_needed_for_size(slab.bin_size) * HEAP_SLAB_SIZE - n
} else {
info.total_memory_used_for_book_keeping += HEAP_SLAB_SIZE - n
}
remote_free_bins := 0
for j in 0..<slab.sectors {
remote_free_bins += int(intrinsics.count_ones(intrinsics.atomic_load_explicit(&slab.remote_free[j], .Seq_Cst)))
}
info.total_remote_free_bins += remote_free_bins
info.total_memory_remotely_free += slab.bin_size * remote_free_bins
} else {
// When the slab is allocated, the book-keeping bitmaps and
// the Slab struct itself will take some of this space, so
// it's only an approximation of what is possible.
info.total_memory_free += HEAP_SLAB_SIZE
when !ODIN_DISABLE_ASSERT {
if !slab.is_dirty {
// Verify that the slab is actually zeroed out ahead of its index field.
ptr := cast([^]u8)rawptr(uintptr(slab) + size_of(int))
for k in 0..<HEAP_SLAB_SIZE - size_of(int) {
assert_contextless(ptr[k] == 0)
}
}
}
}
if slab.bin_size > HEAP_MAX_BIN_SIZE {
// Skip contiguous slabs.
i += heap_slabs_needed_for_size(slab.bin_size)
} else {
i += 1
}
}
// Every superpage has to sacrifice one Slab's worth of space so
// that they're all aligned.
info.total_memory_used_for_book_keeping += HEAP_SLAB_SIZE
info.total_slabs += HEAP_SLAB_COUNT
}
superpage = superpage.next
}
assert_contextless(info.total_memory_allocated_from_system == info.total_memory_used_for_book_keeping + info.total_memory_in_use + info.total_memory_free, "The heap allocator's metrics for total memory in use, free, and used for book-keeping do not add up to the total memory allocated from the operating system.")
if local_heap_cache != nil {
assert_contextless(info.total_superpages == local_heap_cache.owned_superpages)
}
return
}

View File

@@ -74,7 +74,7 @@ _set_env :: proc(key, v_new: string) -> Error {
// wasn't in the environment in the first place.
k_addr, v_addr := _kv_addr_from_val(v_curr, key)
if len(v_new) > len(v_curr) {
k_addr = ([^]u8)(runtime.heap_resize(k_addr, kv_size))
k_addr = ([^]u8)(runtime.heap_resize(k_addr, len(v_curr), kv_size))
if k_addr == nil {
return .Out_Of_Memory
}

View File

@@ -0,0 +1,127 @@
package tests_heap_allocator_libc
import "base:intrinsics"
import "base:runtime"
import "core:mem"
// This package contains the old libc malloc-based allocator, for comparison.
Allocator :: runtime.Allocator
Allocator_Mode :: runtime.Allocator_Mode
Allocator_Mode_Set :: runtime.Allocator_Mode_Set
Allocator_Error :: runtime.Allocator_Error
libc_allocator :: proc() -> Allocator {
return Allocator{
procedure = libc_allocator_proc,
data = nil,
}
}
libc_allocator_proc :: proc(allocator_data: rawptr, mode: Allocator_Mode,
size, alignment: int,
old_memory: rawptr, old_size: int, loc := #caller_location) -> ([]byte, Allocator_Error) {
//
// NOTE(tetra, 2020-01-14): The heap doesn't respect alignment.
// Instead, we overallocate by `alignment + size_of(rawptr) - 1`, and insert
// padding. We also store the original pointer returned by heap_alloc right before
// the pointer we return to the user.
//
aligned_alloc :: proc(size, alignment: int, old_ptr: rawptr, old_size: int, zero_memory := true) -> ([]byte, Allocator_Error) {
// Not(flysand): We need to reserve enough space for alignment, which
// includes the user data itself, the space to store the pointer to
// allocation start, as well as the padding required to align both
// the user data and the pointer.
a := max(alignment, align_of(rawptr))
space := a-1 + size_of(rawptr) + size
allocated_mem: rawptr
force_copy := old_ptr != nil && alignment > align_of(rawptr)
if old_ptr != nil && !force_copy {
original_old_ptr := ([^]rawptr)(old_ptr)[-1]
allocated_mem = heap_resize(original_old_ptr, space)
} else {
allocated_mem = heap_alloc(space, zero_memory)
}
aligned_mem := rawptr(([^]u8)(allocated_mem)[size_of(rawptr):])
ptr := uintptr(aligned_mem)
aligned_ptr := (ptr + uintptr(a)-1) & ~(uintptr(a)-1)
if allocated_mem == nil {
aligned_free(old_ptr)
aligned_free(allocated_mem)
return nil, .Out_Of_Memory
}
aligned_mem = rawptr(aligned_ptr)
([^]rawptr)(aligned_mem)[-1] = allocated_mem
if force_copy {
runtime.mem_copy_non_overlapping(aligned_mem, old_ptr, min(old_size, size))
aligned_free(old_ptr)
}
return mem.byte_slice(aligned_mem, size), nil
}
aligned_free :: proc(p: rawptr) {
if p != nil {
heap_free(([^]rawptr)(p)[-1])
}
}
aligned_resize :: proc(p: rawptr, old_size: int, new_size: int, new_alignment: int, zero_memory := true) -> (new_memory: []byte, err: Allocator_Error) {
if p == nil {
return aligned_alloc(new_size, new_alignment, nil, old_size, zero_memory)
}
new_memory = aligned_alloc(new_size, new_alignment, p, old_size, zero_memory) or_return
// NOTE: heap_resize does not zero the new memory, so we do it
if zero_memory && new_size > old_size {
new_region := raw_data(new_memory[old_size:])
intrinsics.mem_zero(new_region, new_size - old_size)
}
return
}
switch mode {
case .Alloc, .Alloc_Non_Zeroed:
return aligned_alloc(size, alignment, nil, 0, mode == .Alloc)
case .Free:
aligned_free(old_memory)
case .Free_All:
return nil, .Mode_Not_Implemented
case .Resize, .Resize_Non_Zeroed:
return aligned_resize(old_memory, old_size, size, alignment, mode == .Resize)
case .Query_Features:
set := (^Allocator_Mode_Set)(old_memory)
if set != nil {
set^ = {.Alloc, .Alloc_Non_Zeroed, .Free, .Resize, .Resize_Non_Zeroed, .Query_Features}
}
return nil, nil
case .Query_Info:
return nil, .Mode_Not_Implemented
}
return nil, nil
}
heap_alloc :: proc "contextless" (size: int, zero_memory := true) -> rawptr {
return _heap_alloc(size, zero_memory)
}
heap_resize :: proc "contextless" (ptr: rawptr, new_size: int) -> rawptr {
return _heap_resize(ptr, new_size)
}
heap_free :: proc "contextless" (ptr: rawptr) {
_heap_free(ptr)
}

View File

@@ -1,6 +1,6 @@
#+build orca
#+private
package runtime
package tests_heap_allocator_libc
foreign {
@(link_name="malloc") _orca_malloc :: proc "c" (size: int) -> rawptr ---

View File

@@ -1,6 +1,6 @@
#+build js, wasi, freestanding, essence
#+private
package runtime
package tests_heap_allocator_libc
_heap_alloc :: proc "contextless" (size: int, zero_memory := true) -> rawptr {
context = default_context()

View File

@@ -1,6 +1,6 @@
#+build linux, darwin, freebsd, openbsd, netbsd, haiku
#+private
package runtime
package tests_heap_allocator_libc
when ODIN_OS == .Darwin {
foreign import libc "system:System.framework"

View File

@@ -1,4 +1,4 @@
package runtime
package tests_heap_allocator_libc
import "../sanitizer"

File diff suppressed because it is too large Load Diff