Refactor core:mem/tlsf, add free_all support.

TODO: Allow the TLSF allocator to add additional pools when it would ordinarily OOM
      by calling its backing allocator.
This commit is contained in:
Jeroen van Rijn
2025-04-14 17:13:27 +02:00
parent 0e9cd0fb6a
commit 7088284ff4
3 changed files with 569 additions and 445 deletions
+29 -11
View File
@@ -10,6 +10,7 @@
// package mem_tlsf implements a Two Level Segregated Fit memory allocator.
package mem_tlsf
import "base:intrinsics"
import "base:runtime"
Error :: enum byte {
@@ -42,7 +43,7 @@ Allocator :: struct {
// If we're expected to grow when we run out of memory,
// how much should we ask the backing allocator for?
new_pool_size: int,
new_pool_size: uint,
}
#assert(size_of(Allocator) % ALIGN_SIZE == 0)
@@ -74,12 +75,11 @@ init_from_buffer :: proc(control: ^Allocator, buf: []byte) -> Error {
allocator = {},
}
clear(control)
return pool_add(control, buf[:])
return free_all(control)
}
@(require_results)
init_from_allocator :: proc(control: ^Allocator, backing: runtime.Allocator, initial_pool_size: int, new_pool_size := 0) -> Error {
init_from_allocator :: proc(control: ^Allocator, backing: runtime.Allocator, initial_pool_size: int) -> Error {
assert(control != nil)
pool_bytes := align_up(uint(initial_pool_size), ALIGN_SIZE) + INITIAL_POOL_OVERHEAD
if pool_bytes < BLOCK_SIZE_MIN {
@@ -98,8 +98,9 @@ init_from_allocator :: proc(control: ^Allocator, backing: runtime.Allocator, ini
allocator = backing,
}
clear(control)
return pool_add(control, buf[:])
// TODO(Jeroen): Add automatically growing the pools from the backing allocator
return free_all(control)
}
init :: proc{init_from_buffer, init_from_allocator}
@@ -112,8 +113,6 @@ destroy :: proc(control: ^Allocator) {
// No need to call `pool_remove` or anything, as they're they're embedded in the backing memory.
// We do however need to free the `Pool` tracking entities and the backing memory itself.
// As `Allocator` is embedded in the first backing slice, the `control` pointer will be
// invalid after this call.
for p := control.pool.next; p != nil; {
next := p.next
@@ -145,9 +144,8 @@ allocator_proc :: proc(allocator_data: rawptr, mode: runtime.Allocator_Mode,
return nil, nil
case .Free_All:
// NOTE: this doesn't work right at the moment, Jeroen has it on his to-do list :)
// clear(control)
return nil, .Mode_Not_Implemented
free_all(control)
return nil, nil
case .Resize:
return resize(control, old_memory, uint(old_size), uint(size), uint(alignment))
@@ -168,3 +166,23 @@ allocator_proc :: proc(allocator_data: rawptr, mode: runtime.Allocator_Mode,
return nil, nil
}
// Exported solely to facilitate testing
@(require_results)
ffs :: proc "contextless" (word: u32) -> (bit: i32) {
return -1 if word == 0 else i32(intrinsics.count_trailing_zeros(word))
}
// Exported solely to facilitate testing
@(require_results)
fls :: proc "contextless" (word: u32) -> (bit: i32) {
N :: (size_of(u32) * 8) - 1
return i32(N - intrinsics.count_leading_zeros(word))
}
// Exported solely to facilitate testing
@(require_results)
fls_uint :: proc "contextless" (size: uint) -> (bit: i32) {
N :: (size_of(uint) * 8) - 1
return i32(N - intrinsics.count_leading_zeros(size))
}
+438 -434
View File
@@ -7,7 +7,6 @@
Jeroen van Rijn: Source port
*/
package mem_tlsf
import "base:intrinsics"
@@ -106,436 +105,12 @@ bits for `FL_INDEX`.
BLOCK_SIZE_MIN :: uint(size_of(Block_Header) - size_of(^Block_Header))
BLOCK_SIZE_MAX :: uint(1) << FL_INDEX_MAX
/*
TLSF achieves O(1) cost for `alloc` and `free` operations by limiting
the search for a free block to a free list of guaranteed size
adequate to fulfill the request, combined with efficient free list
queries using bitmasks and architecture-specific bit-manipulation
routines.
NOTE: TLSF spec relies on ffs/fls returning a value in the range 0..31.
*/
@(require_results)
ffs :: proc "contextless" (word: u32) -> (bit: i32) {
return -1 if word == 0 else i32(intrinsics.count_trailing_zeros(word))
}
@(require_results)
fls :: proc "contextless" (word: u32) -> (bit: i32) {
N :: (size_of(u32) * 8) - 1
return i32(N - intrinsics.count_leading_zeros(word))
}
@(require_results)
fls_uint :: proc "contextless" (size: uint) -> (bit: i32) {
N :: (size_of(uint) * 8) - 1
return i32(N - intrinsics.count_leading_zeros(size))
}
@(require_results)
block_size :: proc "contextless" (block: ^Block_Header) -> (size: uint) {
return block.size &~ (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE)
}
block_set_size :: proc "contextless" (block: ^Block_Header, size: uint) {
old_size := block.size
block.size = size | (old_size & (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE))
}
@(require_results)
block_is_last :: proc "contextless" (block: ^Block_Header) -> (is_last: bool) {
return block_size(block) == 0
}
@(require_results)
block_is_free :: proc "contextless" (block: ^Block_Header) -> (is_free: bool) {
return (block.size & BLOCK_HEADER_FREE) == BLOCK_HEADER_FREE
}
block_set_free :: proc "contextless" (block: ^Block_Header) {
block.size |= BLOCK_HEADER_FREE
}
block_set_used :: proc "contextless" (block: ^Block_Header) {
block.size &~= BLOCK_HEADER_FREE
}
@(require_results)
block_is_prev_free :: proc "contextless" (block: ^Block_Header) -> (is_prev_free: bool) {
return (block.size & BLOCK_HEADER_PREV_FREE) == BLOCK_HEADER_PREV_FREE
}
block_set_prev_free :: proc "contextless" (block: ^Block_Header) {
block.size |= BLOCK_HEADER_PREV_FREE
}
block_set_prev_used :: proc "contextless" (block: ^Block_Header) {
block.size &~= BLOCK_HEADER_PREV_FREE
}
@(require_results)
block_from_ptr :: proc(ptr: rawptr) -> (block_ptr: ^Block_Header) {
return (^Block_Header)(uintptr(ptr) - BLOCK_START_OFFSET)
}
@(require_results)
block_to_ptr :: proc(block: ^Block_Header) -> (ptr: rawptr) {
return rawptr(uintptr(block) + BLOCK_START_OFFSET)
}
// Return location of next block after block of given size.
@(require_results)
offset_to_block :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) {
return (^Block_Header)(uintptr(ptr) + uintptr(size))
}
@(require_results)
offset_to_block_backwards :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) {
return (^Block_Header)(uintptr(ptr) - uintptr(size))
}
// Return location of previous block.
@(require_results)
block_prev :: proc(block: ^Block_Header) -> (prev: ^Block_Header) {
assert(block_is_prev_free(block), "previous block must be free")
return block.prev_phys_block
}
// Return location of next existing block.
@(require_results)
block_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) {
return offset_to_block(block_to_ptr(block), block_size(block) - BLOCK_HEADER_OVERHEAD)
}
// Link a new block with its physical neighbor, return the neighbor.
@(require_results)
block_link_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) {
next = block_next(block)
next.prev_phys_block = block
return
}
block_mark_as_free :: proc(block: ^Block_Header) {
// Link the block to the next block, first.
next := block_link_next(block)
block_set_prev_free(next)
block_set_free(block)
}
block_mark_as_used :: proc(block: ^Block_Header) {
next := block_next(block)
block_set_prev_used(next)
block_set_used(block)
}
@(require_results)
align_up :: proc(x, align: uint) -> (aligned: uint) {
assert(0 == (align & (align - 1)), "must align to a power of two")
return (x + (align - 1)) &~ (align - 1)
}
@(require_results)
align_down :: proc(x, align: uint) -> (aligned: uint) {
assert(0 == (align & (align - 1)), "must align to a power of two")
return x - (x & (align - 1))
}
@(require_results)
align_ptr :: proc(ptr: rawptr, align: uint) -> (aligned: rawptr) {
assert(0 == (align & (align - 1)), "must align to a power of two")
align_mask := uintptr(align) - 1
_ptr := uintptr(ptr)
_aligned := (_ptr + align_mask) &~ (align_mask)
return rawptr(_aligned)
}
// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum.
@(require_results)
adjust_request_size :: proc(size, align: uint) -> (adjusted: uint) {
if size == 0 {
return 0
}
// aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`.
if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX {
adjusted = max(aligned, BLOCK_SIZE_MIN)
}
return
}
// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum.
@(require_results)
adjust_request_size_with_err :: proc(size, align: uint) -> (adjusted: uint, err: runtime.Allocator_Error) {
if size == 0 {
return 0, nil
}
// aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`.
if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX {
adjusted = min(aligned, BLOCK_SIZE_MAX)
} else {
err = .Out_Of_Memory
}
return
}
// TLSF utility functions. In most cases these are direct translations of
// the documentation in the research paper.
@(optimization_mode="favor_size", require_results)
mapping_insert :: proc(size: uint) -> (fl, sl: i32) {
if size < SMALL_BLOCK_SIZE {
// Store small blocks in first list.
sl = i32(size) / (SMALL_BLOCK_SIZE / SL_INDEX_COUNT)
} else {
fl = fls_uint(size)
sl = i32(size >> (uint(fl) - TLSF_SL_INDEX_COUNT_LOG2)) ~ (1 << TLSF_SL_INDEX_COUNT_LOG2)
fl -= (FL_INDEX_SHIFT - 1)
}
return
}
@(optimization_mode="favor_size", require_results)
mapping_round :: #force_inline proc(size: uint) -> (rounded: uint) {
rounded = size
if size >= SMALL_BLOCK_SIZE {
round := uint(1 << (uint(fls_uint(size) - TLSF_SL_INDEX_COUNT_LOG2))) - 1
rounded += round
}
return
}
// This version rounds up to the next block size (for allocations)
@(optimization_mode="favor_size", require_results)
mapping_search :: proc(size: uint) -> (fl, sl: i32) {
return mapping_insert(mapping_round(size))
}
@(require_results)
search_suitable_block :: proc(control: ^Allocator, fli, sli: ^i32) -> (block: ^Block_Header) {
// First, search for a block in the list associated with the given fl/sl index.
fl := fli^; sl := sli^
sl_map := control.sl_bitmap[fli^] & (~u32(0) << uint(sl))
if sl_map == 0 {
// No block exists. Search in the next largest first-level list.
fl_map := control.fl_bitmap & (~u32(0) << uint(fl + 1))
if fl_map == 0 {
// No free blocks available, memory has been exhausted.
return {}
}
fl = ffs(fl_map)
fli^ = fl
sl_map = control.sl_bitmap[fl]
}
assert(sl_map != 0, "internal error - second level bitmap is null")
sl = ffs(sl_map)
sli^ = sl
// Return the first block in the free list.
return control.blocks[fl][sl]
}
// Remove a free block from the free list.
remove_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) {
prev := block.prev_free
next := block.next_free
assert(prev != nil, "prev_free can not be nil")
assert(next != nil, "next_free can not be nil")
next.prev_free = prev
prev.next_free = next
// If this block is the head of the free list, set new head.
if control.blocks[fl][sl] == block {
control.blocks[fl][sl] = next
// If the new head is nil, clear the bitmap
if next == &control.block_null {
control.sl_bitmap[fl] &~= (u32(1) << uint(sl))
// If the second bitmap is now empty, clear the fl bitmap
if control.sl_bitmap[fl] == 0 {
control.fl_bitmap &~= (u32(1) << uint(fl))
}
}
}
}
// Insert a free block into the free block list.
insert_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) {
current := control.blocks[fl][sl]
assert(current != nil, "free lists cannot have a nil entry")
assert(block != nil, "cannot insert a nil entry into the free list")
block.next_free = current
block.prev_free = &control.block_null
current.prev_free = block
assert(block_to_ptr(block) == align_ptr(block_to_ptr(block), ALIGN_SIZE), "block not properly aligned")
// Insert the new block at the head of the list, and mark the first- and second-level bitmaps appropriately.
control.blocks[fl][sl] = block
control.fl_bitmap |= (u32(1) << uint(fl))
control.sl_bitmap[fl] |= (u32(1) << uint(sl))
}
// Remove a given block from the free list.
block_remove :: proc(control: ^Allocator, block: ^Block_Header) {
fl, sl := mapping_insert(block_size(block))
remove_free_block(control, block, fl, sl)
}
// Insert a given block into the free list.
block_insert :: proc(control: ^Allocator, block: ^Block_Header) {
fl, sl := mapping_insert(block_size(block))
insert_free_block(control, block, fl, sl)
}
@(require_results)
block_can_split :: proc(block: ^Block_Header, size: uint) -> (can_split: bool) {
return block_size(block) >= size_of(Block_Header) + size
}
// Split a block into two, the second of which is free.
@(require_results)
block_split :: proc(block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) {
// Calculate the amount of space left in the remaining block.
remaining = offset_to_block(block_to_ptr(block), size - BLOCK_HEADER_OVERHEAD)
remain_size := block_size(block) - (size + BLOCK_HEADER_OVERHEAD)
assert(block_to_ptr(remaining) == align_ptr(block_to_ptr(remaining), ALIGN_SIZE),
"remaining block not aligned properly")
assert(block_size(block) == remain_size + size + BLOCK_HEADER_OVERHEAD)
block_set_size(remaining, remain_size)
assert(block_size(remaining) >= BLOCK_SIZE_MIN, "block split with invalid size")
block_set_size(block, size)
block_mark_as_free(remaining)
return remaining
}
// Absorb a free block's storage into an adjacent previous free block.
@(require_results)
block_absorb :: proc(prev: ^Block_Header, block: ^Block_Header) -> (absorbed: ^Block_Header) {
assert(!block_is_last(prev), "previous block can't be last")
// Note: Leaves flags untouched.
prev.size += block_size(block) + BLOCK_HEADER_OVERHEAD
_ = block_link_next(prev)
return prev
}
// Merge a just-freed block with an adjacent previous free block.
@(require_results)
block_merge_prev :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) {
merged = block
if (block_is_prev_free(block)) {
prev := block_prev(block)
assert(prev != nil, "prev physical block can't be nil")
assert(block_is_free(prev), "prev block is not free though marked as such")
block_remove(control, prev)
merged = block_absorb(prev, block)
}
return merged
}
// Merge a just-freed block with an adjacent free block.
@(require_results)
block_merge_next :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) {
merged = block
next := block_next(block)
assert(next != nil, "next physical block can't be nil")
if (block_is_free(next)) {
assert(!block_is_last(block), "previous block can't be last")
block_remove(control, next)
merged = block_absorb(block, next)
}
return merged
}
// Trim any trailing block space off the end of a free block, return to pool.
block_trim_free :: proc(control: ^Allocator, block: ^Block_Header, size: uint) {
assert(block_is_free(block), "block must be free")
if (block_can_split(block, size)) {
remaining_block := block_split(block, size)
_ = block_link_next(block)
block_set_prev_free(remaining_block)
block_insert(control, remaining_block)
}
}
// Trim any trailing block space off the end of a used block, return to pool.
block_trim_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) {
assert(!block_is_free(block), "Block must be used")
if (block_can_split(block, size)) {
// If the next block is free, we must coalesce.
remaining_block := block_split(block, size)
block_set_prev_used(remaining_block)
remaining_block = block_merge_next(control, remaining_block)
block_insert(control, remaining_block)
}
}
// Trim leading block space, return to pool.
@(require_results)
block_trim_free_leading :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) {
remaining = block
if block_can_split(block, size) {
// We want the 2nd block.
remaining = block_split(block, size - BLOCK_HEADER_OVERHEAD)
block_set_prev_free(remaining)
_ = block_link_next(block)
block_insert(control, block)
}
return remaining
}
@(require_results)
block_locate_free :: proc(control: ^Allocator, size: uint) -> (block: ^Block_Header) {
fl, sl: i32
if size != 0 {
fl, sl = mapping_search(size)
/*
`mapping_search` can futz with the size, so for excessively large sizes it can sometimes wind up
with indices that are off the end of the block array. So, we protect against that here,
since this is the only call site of `mapping_search`. Note that we don't need to check `sl`,
as it comes from a modulo operation that guarantees it's always in range.
*/
if fl < FL_INDEX_COUNT {
block = search_suitable_block(control, &fl, &sl)
}
}
if block != nil {
assert(block_size(block) >= size)
remove_free_block(control, block, fl, sl)
}
return block
}
@(require_results)
block_prepare_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (res: []byte, err: runtime.Allocator_Error) {
if block != nil {
assert(size != 0, "Size must be non-zero")
block_trim_free(control, block, size)
block_mark_as_used(block)
res = ([^]byte)(block_to_ptr(block))[:size]
}
return
}
// Clear control structure and point all empty lists at the null block
clear :: proc(control: ^Allocator) {
@(private)
free_all :: proc(control: ^Allocator) -> (err: Error) {
// Clear internal structures
control.block_null.next_free = &control.block_null
control.block_null.prev_free = &control.block_null
control.fl_bitmap = 0
for i in 0..<FL_INDEX_COUNT {
control.sl_bitmap[i] = 0
@@ -543,9 +118,15 @@ clear :: proc(control: ^Allocator) {
control.blocks[i][j] = &control.block_null
}
}
// Add backing pool(s)
for p := &control.pool; p != nil; p = p.next {
pool_add(control, p.data) or_return
}
return
}
@(require_results)
@(private, require_results)
pool_add :: proc(control: ^Allocator, pool: []u8) -> (err: Error) {
assert(uintptr(raw_data(pool)) % ALIGN_SIZE == 0, "Added memory must be aligned")
@@ -573,9 +154,11 @@ pool_add :: proc(control: ^Allocator, pool: []u8) -> (err: Error) {
block_set_size(next, 0)
block_set_used(next)
block_set_prev_free(next)
return
}
@(private)
pool_remove :: proc(control: ^Allocator, pool: []u8) {
block := offset_to_block_backwards(raw_data(pool), BLOCK_HEADER_OVERHEAD)
@@ -587,7 +170,7 @@ pool_remove :: proc(control: ^Allocator, pool: []u8) {
remove_free_block(control, block, fl, sl)
}
@(require_results)
@(private, require_results)
alloc_bytes_non_zeroed :: proc(control: ^Allocator, size: uint, align: uint) -> (res: []byte, err: runtime.Allocator_Error) {
assert(control != nil)
adjust := adjust_request_size(size, ALIGN_SIZE)
@@ -600,7 +183,7 @@ alloc_bytes_non_zeroed :: proc(control: ^Allocator, size: uint, align: uint) ->
return nil, .Out_Of_Memory
}
block := block_locate_free(control, aligned_size)
block := block_locate_free(control, aligned_size)
if block == nil {
return nil, .Out_Of_Memory
}
@@ -626,7 +209,7 @@ alloc_bytes_non_zeroed :: proc(control: ^Allocator, size: uint, align: uint) ->
return block_prepare_used(control, block, adjust)
}
@(require_results)
@(private, require_results)
alloc_bytes :: proc(control: ^Allocator, size: uint, align: uint) -> (res: []byte, err: runtime.Allocator_Error) {
res, err = alloc_bytes_non_zeroed(control, size, align)
if err == nil {
@@ -652,7 +235,7 @@ free_with_size :: proc(control: ^Allocator, ptr: rawptr, size: uint) {
}
@(require_results)
@(private, require_results)
resize :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, alignment: uint) -> (res: []byte, err: runtime.Allocator_Error) {
assert(control != nil)
if ptr != nil && new_size == 0 {
@@ -696,7 +279,7 @@ resize :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, align
return
}
@(require_results)
@(private, require_results)
resize_non_zeroed :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size: uint, alignment: uint) -> (res: []byte, err: runtime.Allocator_Error) {
assert(control != nil)
if ptr != nil && new_size == 0 {
@@ -735,3 +318,424 @@ resize_non_zeroed :: proc(control: ^Allocator, ptr: rawptr, old_size, new_size:
res = ([^]byte)(ptr)[:new_size]
return
}
/*
TLSF achieves O(1) cost for `alloc` and `free` operations by limiting
the search for a free block to a free list of guaranteed size
adequate to fulfill the request, combined with efficient free list
queries using bitmasks and architecture-specific bit-manipulation
routines.
NOTE: TLSF spec relies on ffs/fls returning a value in the range 0..31.
*/
@(private, require_results)
block_size :: proc "contextless" (block: ^Block_Header) -> (size: uint) {
return block.size &~ (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE)
}
@(private)
block_set_size :: proc "contextless" (block: ^Block_Header, size: uint) {
old_size := block.size
block.size = size | (old_size & (BLOCK_HEADER_FREE | BLOCK_HEADER_PREV_FREE))
}
@(private, require_results)
block_is_last :: proc "contextless" (block: ^Block_Header) -> (is_last: bool) {
return block_size(block) == 0
}
@(private, require_results)
block_is_free :: proc "contextless" (block: ^Block_Header) -> (is_free: bool) {
return (block.size & BLOCK_HEADER_FREE) == BLOCK_HEADER_FREE
}
@(private)
block_set_free :: proc "contextless" (block: ^Block_Header) {
block.size |= BLOCK_HEADER_FREE
}
@(private)
block_set_used :: proc "contextless" (block: ^Block_Header) {
block.size &~= BLOCK_HEADER_FREE
}
@(private, require_results)
block_is_prev_free :: proc "contextless" (block: ^Block_Header) -> (is_prev_free: bool) {
return (block.size & BLOCK_HEADER_PREV_FREE) == BLOCK_HEADER_PREV_FREE
}
@(private)
block_set_prev_free :: proc "contextless" (block: ^Block_Header) {
block.size |= BLOCK_HEADER_PREV_FREE
}
@(private)
block_set_prev_used :: proc "contextless" (block: ^Block_Header) {
block.size &~= BLOCK_HEADER_PREV_FREE
}
@(private, require_results)
block_from_ptr :: proc(ptr: rawptr) -> (block_ptr: ^Block_Header) {
return (^Block_Header)(uintptr(ptr) - BLOCK_START_OFFSET)
}
@(private, require_results)
block_to_ptr :: proc(block: ^Block_Header) -> (ptr: rawptr) {
return rawptr(uintptr(block) + BLOCK_START_OFFSET)
}
// Return location of next block after block of given size.
@(private, require_results)
offset_to_block :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) {
return (^Block_Header)(uintptr(ptr) + uintptr(size))
}
@(private, require_results)
offset_to_block_backwards :: proc(ptr: rawptr, size: uint) -> (block: ^Block_Header) {
return (^Block_Header)(uintptr(ptr) - uintptr(size))
}
// Return location of previous block.
@(private, require_results)
block_prev :: proc(block: ^Block_Header) -> (prev: ^Block_Header) {
assert(block_is_prev_free(block), "previous block must be free")
return block.prev_phys_block
}
// Return location of next existing block.
@(private, require_results)
block_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) {
return offset_to_block(block_to_ptr(block), block_size(block) - BLOCK_HEADER_OVERHEAD)
}
// Link a new block with its physical neighbor, return the neighbor.
@(private, require_results)
block_link_next :: proc(block: ^Block_Header) -> (next: ^Block_Header) {
next = block_next(block)
next.prev_phys_block = block
return
}
@(private)
block_mark_as_free :: proc(block: ^Block_Header) {
// Link the block to the next block, first.
next := block_link_next(block)
block_set_prev_free(next)
block_set_free(block)
}
@(private)
block_mark_as_used :: proc(block: ^Block_Header) {
next := block_next(block)
block_set_prev_used(next)
block_set_used(block)
}
@(private, require_results)
align_up :: proc(x, align: uint) -> (aligned: uint) {
assert(0 == (align & (align - 1)), "must align to a power of two")
return (x + (align - 1)) &~ (align - 1)
}
@(private, require_results)
align_down :: proc(x, align: uint) -> (aligned: uint) {
assert(0 == (align & (align - 1)), "must align to a power of two")
return x - (x & (align - 1))
}
@(private, require_results)
align_ptr :: proc(ptr: rawptr, align: uint) -> (aligned: rawptr) {
assert(0 == (align & (align - 1)), "must align to a power of two")
align_mask := uintptr(align) - 1
_ptr := uintptr(ptr)
_aligned := (_ptr + align_mask) &~ (align_mask)
return rawptr(_aligned)
}
// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum.
@(private, require_results)
adjust_request_size :: proc(size, align: uint) -> (adjusted: uint) {
if size == 0 {
return 0
}
// aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`.
if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX {
adjusted = max(aligned, BLOCK_SIZE_MIN)
}
return
}
// Adjust an allocation size to be aligned to word size, and no smaller than internal minimum.
@(private, require_results)
adjust_request_size_with_err :: proc(size, align: uint) -> (adjusted: uint, err: runtime.Allocator_Error) {
if size == 0 {
return 0, nil
}
// aligned size must not exceed `BLOCK_SIZE_MAX`, or we'll go out of bounds on `sl_bitmap`.
if aligned := align_up(size, align); aligned < BLOCK_SIZE_MAX {
adjusted = min(aligned, BLOCK_SIZE_MAX)
} else {
err = .Out_Of_Memory
}
return
}
// TLSF utility functions. In most cases these are direct translations of
// the documentation in the research paper.
@(optimization_mode="favor_size", private, require_results)
mapping_insert :: proc(size: uint) -> (fl, sl: i32) {
if size < SMALL_BLOCK_SIZE {
// Store small blocks in first list.
sl = i32(size) / (SMALL_BLOCK_SIZE / SL_INDEX_COUNT)
} else {
fl = fls_uint(size)
sl = i32(size >> (uint(fl) - TLSF_SL_INDEX_COUNT_LOG2)) ~ (1 << TLSF_SL_INDEX_COUNT_LOG2)
fl -= (FL_INDEX_SHIFT - 1)
}
return
}
@(optimization_mode="favor_size", private, require_results)
mapping_round :: #force_inline proc(size: uint) -> (rounded: uint) {
rounded = size
if size >= SMALL_BLOCK_SIZE {
round := uint(1 << (uint(fls_uint(size) - TLSF_SL_INDEX_COUNT_LOG2))) - 1
rounded += round
}
return
}
// This version rounds up to the next block size (for allocations)
@(optimization_mode="favor_size", private, require_results)
mapping_search :: proc(size: uint) -> (fl, sl: i32) {
return mapping_insert(mapping_round(size))
}
@(private, require_results)
search_suitable_block :: proc(control: ^Allocator, fli, sli: ^i32) -> (block: ^Block_Header) {
// First, search for a block in the list associated with the given fl/sl index.
fl := fli^; sl := sli^
sl_map := control.sl_bitmap[fli^] & (~u32(0) << uint(sl))
if sl_map == 0 {
// No block exists. Search in the next largest first-level list.
fl_map := control.fl_bitmap & (~u32(0) << uint(fl + 1))
if fl_map == 0 {
// No free blocks available, memory has been exhausted.
return {}
}
fl = ffs(fl_map)
fli^ = fl
sl_map = control.sl_bitmap[fl]
}
assert(sl_map != 0, "internal error - second level bitmap is null")
sl = ffs(sl_map)
sli^ = sl
// Return the first block in the free list.
return control.blocks[fl][sl]
}
// Remove a free block from the free list.
@(private)
remove_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) {
prev := block.prev_free
next := block.next_free
assert(prev != nil, "prev_free can not be nil")
assert(next != nil, "next_free can not be nil")
next.prev_free = prev
prev.next_free = next
// If this block is the head of the free list, set new head.
if control.blocks[fl][sl] == block {
control.blocks[fl][sl] = next
// If the new head is nil, clear the bitmap
if next == &control.block_null {
control.sl_bitmap[fl] &~= (u32(1) << uint(sl))
// If the second bitmap is now empty, clear the fl bitmap
if control.sl_bitmap[fl] == 0 {
control.fl_bitmap &~= (u32(1) << uint(fl))
}
}
}
}
// Insert a free block into the free block list.
@(private)
insert_free_block :: proc(control: ^Allocator, block: ^Block_Header, fl: i32, sl: i32) {
current := control.blocks[fl][sl]
assert(current != nil, "free lists cannot have a nil entry")
assert(block != nil, "cannot insert a nil entry into the free list")
block.next_free = current
block.prev_free = &control.block_null
current.prev_free = block
assert(block_to_ptr(block) == align_ptr(block_to_ptr(block), ALIGN_SIZE), "block not properly aligned")
// Insert the new block at the head of the list, and mark the first- and second-level bitmaps appropriately.
control.blocks[fl][sl] = block
control.fl_bitmap |= (u32(1) << uint(fl))
control.sl_bitmap[fl] |= (u32(1) << uint(sl))
}
// Remove a given block from the free list.
@(private)
block_remove :: proc(control: ^Allocator, block: ^Block_Header) {
fl, sl := mapping_insert(block_size(block))
remove_free_block(control, block, fl, sl)
}
// Insert a given block into the free list.
@(private)
block_insert :: proc(control: ^Allocator, block: ^Block_Header) {
fl, sl := mapping_insert(block_size(block))
insert_free_block(control, block, fl, sl)
}
@(private, require_results)
block_can_split :: proc(block: ^Block_Header, size: uint) -> (can_split: bool) {
return block_size(block) >= size_of(Block_Header) + size
}
// Split a block into two, the second of which is free.
@(private, require_results)
block_split :: proc(block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) {
// Calculate the amount of space left in the remaining block.
remaining = offset_to_block(block_to_ptr(block), size - BLOCK_HEADER_OVERHEAD)
remain_size := block_size(block) - (size + BLOCK_HEADER_OVERHEAD)
assert(block_to_ptr(remaining) == align_ptr(block_to_ptr(remaining), ALIGN_SIZE),
"remaining block not aligned properly")
assert(block_size(block) == remain_size + size + BLOCK_HEADER_OVERHEAD)
block_set_size(remaining, remain_size)
assert(block_size(remaining) >= BLOCK_SIZE_MIN, "block split with invalid size")
block_set_size(block, size)
block_mark_as_free(remaining)
return remaining
}
// Absorb a free block's storage into an adjacent previous free block.
@(private, require_results)
block_absorb :: proc(prev: ^Block_Header, block: ^Block_Header) -> (absorbed: ^Block_Header) {
assert(!block_is_last(prev), "previous block can't be last")
// Note: Leaves flags untouched.
prev.size += block_size(block) + BLOCK_HEADER_OVERHEAD
_ = block_link_next(prev)
return prev
}
// Merge a just-freed block with an adjacent previous free block.
@(private, require_results)
block_merge_prev :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) {
merged = block
if (block_is_prev_free(block)) {
prev := block_prev(block)
assert(prev != nil, "prev physical block can't be nil")
assert(block_is_free(prev), "prev block is not free though marked as such")
block_remove(control, prev)
merged = block_absorb(prev, block)
}
return merged
}
// Merge a just-freed block with an adjacent free block.
@(private, require_results)
block_merge_next :: proc(control: ^Allocator, block: ^Block_Header) -> (merged: ^Block_Header) {
merged = block
next := block_next(block)
assert(next != nil, "next physical block can't be nil")
if (block_is_free(next)) {
assert(!block_is_last(block), "previous block can't be last")
block_remove(control, next)
merged = block_absorb(block, next)
}
return merged
}
// Trim any trailing block space off the end of a free block, return to pool.
@(private)
block_trim_free :: proc(control: ^Allocator, block: ^Block_Header, size: uint) {
assert(block_is_free(block), "block must be free")
if (block_can_split(block, size)) {
remaining_block := block_split(block, size)
_ = block_link_next(block)
block_set_prev_free(remaining_block)
block_insert(control, remaining_block)
}
}
// Trim any trailing block space off the end of a used block, return to pool.
@(private)
block_trim_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) {
assert(!block_is_free(block), "Block must be used")
if (block_can_split(block, size)) {
// If the next block is free, we must coalesce.
remaining_block := block_split(block, size)
block_set_prev_used(remaining_block)
remaining_block = block_merge_next(control, remaining_block)
block_insert(control, remaining_block)
}
}
// Trim leading block space, return to pool.
@(private, require_results)
block_trim_free_leading :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (remaining: ^Block_Header) {
remaining = block
if block_can_split(block, size) {
// We want the 2nd block.
remaining = block_split(block, size - BLOCK_HEADER_OVERHEAD)
block_set_prev_free(remaining)
_ = block_link_next(block)
block_insert(control, block)
}
return remaining
}
@(private, require_results)
block_locate_free :: proc(control: ^Allocator, size: uint) -> (block: ^Block_Header) {
fl, sl: i32
if size != 0 {
fl, sl = mapping_search(size)
/*
`mapping_search` can futz with the size, so for excessively large sizes it can sometimes wind up
with indices that are off the end of the block array. So, we protect against that here,
since this is the only call site of `mapping_search`. Note that we don't need to check `sl`,
as it comes from a modulo operation that guarantees it's always in range.
*/
if fl < FL_INDEX_COUNT {
block = search_suitable_block(control, &fl, &sl)
}
}
if block != nil {
assert(block_size(block) >= size)
remove_free_block(control, block, fl, sl)
}
return block
}
@(private, require_results)
block_prepare_used :: proc(control: ^Allocator, block: ^Block_Header, size: uint) -> (res: []byte, err: runtime.Allocator_Error) {
if block != nil {
assert(size != 0, "Size must be non-zero")
block_trim_free(control, block, size)
block_mark_as_used(block)
res = ([^]byte)(block_to_ptr(block))[:size]
}
return
}
+102
View File
@@ -1,8 +1,10 @@
package test_core_mem
import "core:mem"
import "core:mem/tlsf"
import "core:mem/virtual"
import "core:testing"
import "core:slice"
@test
test_tlsf_bitscan :: proc(t: ^testing.T) {
@@ -54,3 +56,103 @@ test_align_bumping_block_limit :: proc(t: ^testing.T) {
testing.expect_value(t, err, nil)
testing.expect(t, len(data) == 896)
}
@(test)
tlsf_test_overlap_and_zero :: proc(t: ^testing.T) {
default_allocator := context.allocator
alloc: tlsf.Allocator
defer tlsf.destroy(&alloc)
NUM_ALLOCATIONS :: 1_000
BACKING_SIZE :: NUM_ALLOCATIONS * (1_000 + size_of(uintptr))
if err := tlsf.init_from_allocator(&alloc, default_allocator, BACKING_SIZE); err != .None {
testing.fail_now(t, "TLSF init error")
}
context.allocator = tlsf.allocator(&alloc)
allocations := make([dynamic][]byte, 0, NUM_ALLOCATIONS, default_allocator)
defer delete(allocations)
err: mem.Allocator_Error
s: []byte
for size := 1; err == .None && size <= NUM_ALLOCATIONS; size += 1 {
s, err = make([]byte, size)
append(&allocations, s)
}
slice.sort_by(allocations[:len(allocations)], proc(a, b: []byte) -> bool {
return uintptr(raw_data(a)) < uintptr(raw_data((b)))
})
for i in 0..<len(allocations) - 1 {
fail_if_allocations_overlap(t, allocations[i], allocations[i + 1])
fail_if_not_zeroed(t, allocations[i])
}
}
@(test)
tlsf_test_free_all :: proc(t: ^testing.T) {
default_allocator := context.allocator
alloc: tlsf.Allocator
defer tlsf.destroy(&alloc)
NUM_ALLOCATIONS :: 10
ALLOCATION_SIZE :: mem.Megabyte
BACKING_SIZE :: NUM_ALLOCATIONS * (ALLOCATION_SIZE + size_of(uintptr))
if init_err := tlsf.init_from_allocator(&alloc, default_allocator, BACKING_SIZE); init_err != .None {
testing.fail_now(t, "TLSF init error")
}
context.allocator = tlsf.allocator(&alloc)
allocations: [2][dynamic][]byte
allocations[0] = make([dynamic][]byte, 0, NUM_ALLOCATIONS, default_allocator) // After `init`
allocations[1] = make([dynamic][]byte, 0, NUM_ALLOCATIONS, default_allocator) // After `free_all`
defer {
delete(allocations[0])
delete(allocations[1])
}
for {
s := make([]byte, ALLOCATION_SIZE) or_break
append(&allocations[0], s)
}
testing.expect(t, len(allocations[0]) >= 10)
free_all(tlsf.allocator(&alloc))
for {
s := make([]byte, ALLOCATION_SIZE) or_break
append(&allocations[1], s)
}
testing.expect(t, len(allocations[1]) >= 10)
for i in 0..<len(allocations[0]) {
s0, s1 := allocations[0][i], allocations[1][i]
assert(raw_data(s0) == raw_data((s1)))
assert(len(s0) == len((s1)))
}
}
fail_if_not_zeroed :: proc(t: ^testing.T, a: []byte) {
for b in a {
if b != 0 {
testing.fail_now(t, "Allocation wasn't zeroed")
}
}
}
fail_if_allocations_overlap :: proc(t: ^testing.T, a, b: []byte) {
a, b := a, b
a_start := uintptr(raw_data(a))
a_end := a_start + uintptr(len(a))
b_start := uintptr(raw_data(b))
b_end := b_start + uintptr(len(b))
if a_end >= b_end && b_end >= a_start {
testing.fail_now(t, "Allocations overlapped")
}
}