| // Copyright 2026 The IREE Authors |
| // |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| #include "iree/hal/memory/tlsf.h" |
| |
| #include "iree/base/internal/math.h" |
| |
| //===----------------------------------------------------------------------===// |
| // Internal helpers |
| //===----------------------------------------------------------------------===// |
| |
| // Returns a pointer to the block node at |index| via stride arithmetic. |
| static inline iree_hal_memory_tlsf_block_t* iree_hal_memory_tlsf_block_at( |
| const iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t index) { |
| return (iree_hal_memory_tlsf_block_t*)(tlsf->block_storage + |
| (iree_host_size_t)index * |
| tlsf->block_stride); |
| } |
| |
| // Returns a pointer to the inline frontier of a block node. |
| static inline iree_async_frontier_t* iree_hal_memory_tlsf_block_frontier( |
| const iree_hal_memory_tlsf_t* tlsf, iree_hal_memory_tlsf_block_t* block) { |
| return (iree_async_frontier_t*)((uint8_t*)block + tlsf->frontier_offset); |
| } |
| |
| // Grows the block node pool using 2x doubling. New nodes are linked into the |
| // unused_node_head free list. Uses iree_allocator_grow_array for |
| // overflow-checked capacity doubling and realloc. |
| static iree_status_t iree_hal_memory_tlsf_grow_pool( |
| iree_hal_memory_tlsf_t* tlsf) { |
| if (tlsf->block_capacity >= UINT32_MAX) { |
| return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED, |
| "block pool at maximum capacity (%" PRIu32 ")", |
| (uint32_t)UINT32_MAX); |
| } |
| iree_host_size_t old_capacity = tlsf->block_capacity; |
| iree_host_size_t new_capacity = old_capacity; |
| IREE_RETURN_IF_ERROR(iree_allocator_grow_array( |
| tlsf->host_allocator, /*minimum_capacity=*/old_capacity + 1, |
| tlsf->block_stride, &new_capacity, (void**)&tlsf->block_storage)); |
| // Cap at UINT32_MAX since block indices are uint32_t. |
| if (new_capacity > UINT32_MAX) new_capacity = UINT32_MAX; |
| // Initialize new nodes and link them into the unused list. |
| for (iree_host_size_t i = old_capacity; i < new_capacity; ++i) { |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, (uint32_t)i); |
| memset(block, 0, tlsf->block_stride); |
| block->next_free = tlsf->unused_node_head; |
| tlsf->unused_node_head = (uint32_t)i; |
| } |
| tlsf->block_capacity = new_capacity; |
| return iree_ok_status(); |
| } |
| |
| // Allocates a block node from the pool. Grows the pool if no unused nodes |
| // are available. |
| static iree_status_t iree_hal_memory_tlsf_alloc_node( |
| iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t* out_index) { |
| if (tlsf->unused_node_head == IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| IREE_RETURN_IF_ERROR(iree_hal_memory_tlsf_grow_pool(tlsf)); |
| } |
| iree_hal_memory_tlsf_block_index_t index = tlsf->unused_node_head; |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, index); |
| tlsf->unused_node_head = block->next_free; |
| memset(block, 0, tlsf->block_stride); |
| *out_index = index; |
| return iree_ok_status(); |
| } |
| |
| // Returns a block node to the unused pool. |
| static void iree_hal_memory_tlsf_free_node( |
| iree_hal_memory_tlsf_t* tlsf, iree_hal_memory_tlsf_block_index_t index) { |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, index); |
| memset(block, 0, tlsf->block_stride); |
| block->next_free = tlsf->unused_node_head; |
| tlsf->unused_node_head = index; |
| } |
| |
| // Maps a block length to (FL, SL) indices for insertion into the free list |
| // matrix. The block goes into the bin whose range contains |length|. |
| static inline void iree_hal_memory_tlsf_mapping_insert( |
| iree_device_size_t length, uint8_t* out_fl, uint8_t* out_sl) { |
| // FL = position of the most significant set bit. |
| int fl = 63 - iree_math_count_leading_zeros_u64(length); |
| // SL = next SL_LOG2 bits below the MSB. |
| int sl_shift = (fl > IREE_HAL_MEMORY_TLSF_SL_LOG2) |
| ? (fl - IREE_HAL_MEMORY_TLSF_SL_LOG2) |
| : 0; |
| *out_fl = (uint8_t)fl; |
| *out_sl = |
| (uint8_t)((length >> sl_shift) & (IREE_HAL_MEMORY_TLSF_SL_COUNT - 1)); |
| } |
| |
| // Maps a requested allocation length to (FL, SL) indices for searching the |
| // free list matrix. We want the smallest bin that could contain a block >= |
| // |length|, so we round up within the current FL level's SL range. |
| static inline void iree_hal_memory_tlsf_mapping_search( |
| iree_device_size_t length, uint8_t* out_fl, uint8_t* out_sl) { |
| // For sizes that span SL bins within an FL level, round up to ensure we |
| // find a block that is at least |length|. We add (1 << sl_shift) - 1 |
| // before extracting the SL index, which effectively rounds up. |
| int fl = 63 - iree_math_count_leading_zeros_u64(length); |
| if (fl > IREE_HAL_MEMORY_TLSF_SL_LOG2) { |
| int sl_shift = fl - IREE_HAL_MEMORY_TLSF_SL_LOG2; |
| iree_device_size_t rounded = |
| length + (((iree_device_size_t)1 << sl_shift) - 1); |
| // If rounding caused overflow into the next FL level, use that instead. |
| int new_fl = 63 - iree_math_count_leading_zeros_u64(rounded); |
| if (new_fl != fl) { |
| *out_fl = (uint8_t)new_fl; |
| *out_sl = 0; |
| return; |
| } |
| *out_fl = (uint8_t)fl; |
| *out_sl = |
| (uint8_t)((rounded >> sl_shift) & (IREE_HAL_MEMORY_TLSF_SL_COUNT - 1)); |
| } else { |
| *out_fl = (uint8_t)fl; |
| *out_sl = (uint8_t)(length & (IREE_HAL_MEMORY_TLSF_SL_COUNT - 1)); |
| } |
| } |
| |
| // Inserts a free block into the appropriate (FL, SL) free list and updates |
| // the bitmaps. |
| static void iree_hal_memory_tlsf_insert_free_block( |
| iree_hal_memory_tlsf_t* tlsf, iree_hal_memory_tlsf_block_index_t index) { |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, index); |
| IREE_ASSERT(block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE); |
| |
| uint8_t fl = 0, sl = 0; |
| iree_hal_memory_tlsf_mapping_insert(block->length, &fl, &sl); |
| |
| // Insert at the head of the free list for this (FL, SL) bin. |
| iree_hal_memory_tlsf_block_index_t old_head = tlsf->free_lists[fl][sl]; |
| block->prev_free = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| block->next_free = old_head; |
| if (old_head != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_at(tlsf, old_head)->prev_free = index; |
| } |
| tlsf->free_lists[fl][sl] = index; |
| |
| // Set bitmap bits. |
| tlsf->fl_bitmap |= (1ull << fl); |
| tlsf->sl_bitmaps[fl] |= (1u << sl); |
| |
| tlsf->free_block_count++; |
| } |
| |
| // Removes a free block from its (FL, SL) free list and updates bitmaps if |
| // the list becomes empty. |
| static void iree_hal_memory_tlsf_remove_free_block( |
| iree_hal_memory_tlsf_t* tlsf, iree_hal_memory_tlsf_block_index_t index) { |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, index); |
| IREE_ASSERT(block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE); |
| |
| uint8_t fl = 0, sl = 0; |
| iree_hal_memory_tlsf_mapping_insert(block->length, &fl, &sl); |
| |
| // Unlink from the doubly-linked free list. |
| if (block->prev_free != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_at(tlsf, block->prev_free)->next_free = |
| block->next_free; |
| } else { |
| // Was the head of the list. |
| tlsf->free_lists[fl][sl] = block->next_free; |
| } |
| if (block->next_free != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_at(tlsf, block->next_free)->prev_free = |
| block->prev_free; |
| } |
| |
| block->prev_free = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| block->next_free = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| |
| // Clear bitmap bits if the list is now empty. |
| if (tlsf->free_lists[fl][sl] == IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| tlsf->sl_bitmaps[fl] &= ~(1u << sl); |
| if (tlsf->sl_bitmaps[fl] == 0) { |
| tlsf->fl_bitmap &= ~(1ull << fl); |
| } |
| } |
| |
| tlsf->free_block_count--; |
| } |
| |
| // Searches for a free block of at least |length| bytes using bitmap scans. |
| // Returns IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE if no suitable block exists. |
| static iree_hal_memory_tlsf_block_index_t |
| iree_hal_memory_tlsf_find_suitable_block(iree_hal_memory_tlsf_t* tlsf, |
| iree_device_size_t length) { |
| uint8_t fl = 0, sl = 0; |
| iree_hal_memory_tlsf_mapping_search(length, &fl, &sl); |
| |
| // Try to find a block in the current FL level at or above the target SL. |
| uint32_t sl_map = tlsf->sl_bitmaps[fl] & (~0u << sl); |
| if (sl_map != 0) { |
| int found_sl = iree_math_count_trailing_zeros_u32(sl_map); |
| return tlsf->free_lists[fl][found_sl]; |
| } |
| |
| // No block in the current FL level; search higher FL levels. |
| // The mask ~0ull << (fl + 1) is UB when fl==63 (shift by 64). Rewrite using |
| // iree_shr: iree_shr(~0ull, 63 - fl) produces a mask with the bottom fl+1 |
| // bits set, and its complement gives us only the FL levels above fl. |
| uint64_t fl_map = tlsf->fl_bitmap & ~iree_shr(~0ull, 63 - fl); |
| if (fl_map == 0) { |
| return IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| } |
| int found_fl = iree_math_count_trailing_zeros_u64(fl_map); |
| // Take the first SL bin in the found FL level. |
| IREE_ASSERT(tlsf->sl_bitmaps[found_fl] != 0); |
| int found_sl = iree_math_count_trailing_zeros_u32(tlsf->sl_bitmaps[found_fl]); |
| return tlsf->free_lists[found_fl][found_sl]; |
| } |
| |
| // Merges |source_frontier| into |target_block|'s inline frontier. If the |
| // merge overflows capacity, marks the target block as tainted and increments |
| // the diagnostic counter. |
| static void iree_hal_memory_tlsf_merge_frontiers( |
| iree_hal_memory_tlsf_t* tlsf, iree_hal_memory_tlsf_block_t* target_block, |
| const iree_async_frontier_t* source_frontier) { |
| if (!source_frontier || source_frontier->entry_count == 0) return; |
| |
| iree_async_frontier_t* target_frontier = |
| iree_hal_memory_tlsf_block_frontier(tlsf, target_block); |
| |
| // If target is already tainted, nothing to do; the frontier is meaningless. |
| if (target_block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED) return; |
| |
| if (!iree_async_frontier_merge(target_frontier, tlsf->frontier_capacity, |
| source_frontier)) { |
| // Overflow: mark tainted and zero the frontier. |
| target_block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED; |
| target_frontier->entry_count = 0; |
| tlsf->tainted_coalesce_count++; |
| } |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // API |
| //===----------------------------------------------------------------------===// |
| |
| iree_status_t iree_hal_memory_tlsf_initialize( |
| iree_hal_memory_tlsf_options_t options, iree_allocator_t host_allocator, |
| iree_hal_memory_tlsf_t* out_tlsf) { |
| IREE_ASSERT_ARGUMENT(out_tlsf); |
| memset(out_tlsf, 0, sizeof(*out_tlsf)); |
| |
| // Validate options. |
| if (options.range_length == 0) { |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, |
| "range_length must be > 0"); |
| } |
| if (options.alignment == 0) { |
| options.alignment = IREE_HAL_MEMORY_TLSF_MIN_ALIGNMENT; |
| } |
| if (options.alignment < IREE_HAL_MEMORY_TLSF_MIN_ALIGNMENT) { |
| return iree_make_status( |
| IREE_STATUS_INVALID_ARGUMENT, |
| "alignment (%" PRIdsz ") must be >= %" PRIdsz, options.alignment, |
| (iree_device_size_t)IREE_HAL_MEMORY_TLSF_MIN_ALIGNMENT); |
| } |
| if (!iree_device_size_is_power_of_two(options.alignment)) { |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, |
| "alignment (%" PRIdsz ") must be a power of two", |
| options.alignment); |
| } |
| if (options.range_length < options.alignment) { |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, |
| "range_length (%" PRIdsz |
| ") must be >= alignment (%" PRIdsz ")", |
| options.range_length, options.alignment); |
| } |
| // Round range_length down to a multiple of alignment. |
| options.range_length = options.range_length & ~(options.alignment - 1); |
| if (options.frontier_capacity == 0) { |
| options.frontier_capacity = IREE_HAL_MEMORY_TLSF_DEFAULT_FRONTIER_CAPACITY; |
| } |
| |
| // Compute block node layout using overflow-checked struct math. |
| // Each block node is: [fixed fields] [padding] [frontier header] [entries] |
| // The frontier must be aligned for its entry type (8-byte aligned). |
| iree_host_size_t frontier_offset = 0; |
| iree_host_size_t block_stride = 0; |
| IREE_RETURN_IF_ERROR(IREE_STRUCT_LAYOUT( |
| sizeof(iree_hal_memory_tlsf_block_t), &block_stride, |
| IREE_STRUCT_FIELD_ALIGNED(1, iree_async_frontier_t, |
| iree_alignof(iree_async_frontier_entry_t), |
| &frontier_offset), |
| IREE_STRUCT_FIELD(options.frontier_capacity, iree_async_frontier_entry_t, |
| NULL))); |
| |
| // Determine initial block pool capacity. |
| iree_host_size_t initial_capacity = options.initial_block_capacity; |
| if (initial_capacity == 0) { |
| initial_capacity = IREE_HAL_MEMORY_TLSF_DEFAULT_INITIAL_BLOCK_CAPACITY; |
| } |
| |
| // Allocate block storage (overflow-checked array allocation). |
| uint8_t* block_storage = NULL; |
| IREE_RETURN_IF_ERROR(iree_allocator_malloc_array( |
| host_allocator, initial_capacity, block_stride, (void**)&block_storage)); |
| |
| // Initialize the TLSF instance. |
| out_tlsf->range_length = options.range_length; |
| out_tlsf->alignment = options.alignment; |
| out_tlsf->fl_min = |
| (uint8_t)iree_math_count_trailing_zeros_u64(options.alignment); |
| out_tlsf->frontier_capacity = options.frontier_capacity; |
| out_tlsf->fl_bitmap = 0; |
| memset(out_tlsf->sl_bitmaps, 0, sizeof(out_tlsf->sl_bitmaps)); |
| for (int fl = 0; fl < IREE_HAL_MEMORY_TLSF_FL_COUNT; ++fl) { |
| for (int sl = 0; sl < IREE_HAL_MEMORY_TLSF_SL_COUNT; ++sl) { |
| out_tlsf->free_lists[fl][sl] = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| } |
| } |
| out_tlsf->block_storage = block_storage; |
| out_tlsf->block_stride = block_stride; |
| out_tlsf->frontier_offset = frontier_offset; |
| out_tlsf->block_count = 0; |
| out_tlsf->block_capacity = initial_capacity; |
| out_tlsf->unused_node_head = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| out_tlsf->bytes_allocated = 0; |
| out_tlsf->bytes_free = options.range_length; |
| out_tlsf->allocation_count = 0; |
| out_tlsf->free_block_count = 0; |
| out_tlsf->tainted_coalesce_count = 0; |
| out_tlsf->host_allocator = host_allocator; |
| |
| // Link all pre-allocated nodes into the unused list (except node 0 which |
| // we will use for the initial free block). |
| for (iree_host_size_t i = 1; i < initial_capacity; ++i) { |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(out_tlsf, (uint32_t)i); |
| block->next_free = out_tlsf->unused_node_head; |
| out_tlsf->unused_node_head = (uint32_t)i; |
| } |
| out_tlsf->block_count = initial_capacity; |
| |
| // Create the initial free block spanning the entire range. |
| iree_hal_memory_tlsf_block_t* initial_block = |
| iree_hal_memory_tlsf_block_at(out_tlsf, 0); |
| initial_block->offset = 0; |
| initial_block->length = options.range_length; |
| initial_block->prev_physical = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| initial_block->next_physical = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| initial_block->prev_free = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| initial_block->next_free = IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| initial_block->flags = IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE | |
| IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST; |
| // Frontier starts empty. |
| iree_async_frontier_t* initial_frontier = |
| iree_hal_memory_tlsf_block_frontier(out_tlsf, initial_block); |
| iree_async_frontier_initialize(initial_frontier, 0); |
| |
| // Insert the initial free block into the appropriate FL/SL bin. |
| iree_hal_memory_tlsf_insert_free_block(out_tlsf, 0); |
| |
| return iree_ok_status(); |
| } |
| |
| void iree_hal_memory_tlsf_deinitialize(iree_hal_memory_tlsf_t* tlsf) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| if (tlsf->allocation_count > 0) { |
| IREE_ASSERT(false, "TLSF deinitialize with %" PRIu32 " leaked allocations", |
| tlsf->allocation_count); |
| } |
| iree_allocator_free(tlsf->host_allocator, tlsf->block_storage); |
| memset(tlsf, 0, sizeof(*tlsf)); |
| } |
| |
| iree_status_t iree_hal_memory_tlsf_try_allocate( |
| iree_hal_memory_tlsf_t* tlsf, iree_device_size_t length, |
| iree_hal_memory_tlsf_allocation_t* out_allocation, |
| iree_hal_memory_tlsf_allocate_result_t* out_result) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| IREE_ASSERT_ARGUMENT(out_allocation); |
| IREE_ASSERT_ARGUMENT(out_result); |
| memset(out_allocation, 0, sizeof(*out_allocation)); |
| *out_result = IREE_HAL_MEMORY_TLSF_ALLOCATE_EXHAUSTED; |
| |
| if (length == 0) { |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, |
| "allocation length must be > 0"); |
| } |
| |
| // Guard against overflow: if length is so large that rounding up to |
| // alignment would wrap around, reject immediately. This prevents |
| // near-SIZE_MAX requests from silently succeeding as tiny allocations. |
| if (length > IREE_DEVICE_SIZE_MAX - (tlsf->alignment - 1)) { |
| return iree_make_status(IREE_STATUS_OUT_OF_RANGE, |
| "allocation length %" PRIdsz |
| " overflows when aligned to %" PRIdsz, |
| length, tlsf->alignment); |
| } |
| |
| // Round up to alignment (and ensure at least minimum block size). |
| iree_device_size_t aligned_length = |
| iree_device_align(length, tlsf->alignment); |
| if (aligned_length < tlsf->alignment) { |
| aligned_length = tlsf->alignment; |
| } |
| |
| // Find a suitable free block. |
| iree_hal_memory_tlsf_block_index_t block_index = |
| iree_hal_memory_tlsf_find_suitable_block(tlsf, aligned_length); |
| if (block_index == IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| return iree_ok_status(); |
| } |
| |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, block_index); |
| |
| iree_device_size_t remainder = block->length - aligned_length; |
| iree_hal_memory_tlsf_block_index_t remainder_index = |
| IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE; |
| if (remainder >= tlsf->alignment) { |
| // Allocate the remainder node before unlinking the selected free block so |
| // metadata growth failure leaves allocator state unchanged. |
| IREE_RETURN_IF_ERROR( |
| iree_hal_memory_tlsf_alloc_node(tlsf, &remainder_index)); |
| } |
| |
| // Remove the block from the free list after all fallible split preparation. |
| iree_hal_memory_tlsf_remove_free_block(tlsf, block_index); |
| |
| // Re-fetch block: alloc_node may have grown the block pool via realloc, |
| // invalidating all prior block pointers into block_storage. |
| block = iree_hal_memory_tlsf_block_at(tlsf, block_index); |
| |
| if (remainder_index != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_t* remainder_block = |
| iree_hal_memory_tlsf_block_at(tlsf, remainder_index); |
| remainder_block->offset = block->offset + aligned_length; |
| remainder_block->length = remainder; |
| remainder_block->prev_physical = block_index; |
| remainder_block->next_physical = block->next_physical; |
| remainder_block->flags = IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE; |
| |
| // Transfer LAST flag if the original block was last. |
| if (block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST) { |
| remainder_block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST; |
| block->flags &= ~IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST; |
| } |
| |
| // Update the old right neighbor's prev_physical to point to the remainder. |
| if (remainder_block->next_physical != |
| IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_at(tlsf, remainder_block->next_physical) |
| ->prev_physical = remainder_index; |
| } |
| |
| // Link the remainder into the physical list. |
| block->next_physical = remainder_index; |
| block->length = aligned_length; |
| |
| // The remainder block gets an empty frontier (it was just split from a |
| // block being allocated; it has no independent usage history). |
| iree_async_frontier_t* remainder_frontier = |
| iree_hal_memory_tlsf_block_frontier(tlsf, remainder_block); |
| iree_async_frontier_initialize(remainder_frontier, 0); |
| |
| // Insert the remainder into the free list. |
| iree_hal_memory_tlsf_insert_free_block(tlsf, remainder_index); |
| |
| // Update free bytes (the remainder stays free). |
| tlsf->bytes_free -= aligned_length; |
| } else { |
| // Cannot split; give the whole block (may be slightly over-sized). |
| tlsf->bytes_free -= block->length; |
| } |
| |
| // Mark the block as allocated: clear the FREE flag before populating the |
| // result so the caller sees only the allocated-state flags (LAST, TAINTED). |
| block->flags &= ~IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE; |
| |
| iree_async_frontier_t* frontier = |
| iree_hal_memory_tlsf_block_frontier(tlsf, block); |
| out_allocation->offset = block->offset; |
| out_allocation->length = block->length; |
| out_allocation->block_index = block_index; |
| out_allocation->death_frontier = |
| (frontier->entry_count > 0) ? frontier : NULL; |
| out_allocation->block_flags = block->flags; |
| |
| tlsf->bytes_allocated += block->length; |
| tlsf->allocation_count++; |
| |
| *out_result = IREE_HAL_MEMORY_TLSF_ALLOCATE_OK; |
| return iree_ok_status(); |
| } |
| |
| iree_status_t iree_hal_memory_tlsf_allocate( |
| iree_hal_memory_tlsf_t* tlsf, iree_device_size_t length, |
| iree_hal_memory_tlsf_allocation_t* out_allocation) { |
| iree_hal_memory_tlsf_allocate_result_t result = |
| IREE_HAL_MEMORY_TLSF_ALLOCATE_EXHAUSTED; |
| IREE_RETURN_IF_ERROR( |
| iree_hal_memory_tlsf_try_allocate(tlsf, length, out_allocation, &result)); |
| if (result == IREE_HAL_MEMORY_TLSF_ALLOCATE_EXHAUSTED) { |
| return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED, |
| "no free block of %" PRIdsz |
| " bytes; largest free block is %" PRIdsz " bytes", |
| length, |
| iree_hal_memory_tlsf_largest_free_block(tlsf)); |
| } |
| return iree_ok_status(); |
| } |
| |
| static void iree_hal_memory_tlsf_free_impl( |
| iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t block_index, |
| const iree_async_frontier_t* death_frontier, bool preserve_block_metadata) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| IREE_ASSERT(block_index != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE); |
| |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, block_index); |
| IREE_ASSERT(!(block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE)); |
| |
| // Update stats for the deallocation. |
| tlsf->bytes_allocated -= block->length; |
| tlsf->bytes_free += block->length; |
| tlsf->allocation_count--; |
| |
| // Mark as free before coalescing. Normal free() replaces the block's |
| // dependency metadata with |death_frontier|. restore() preserves the current |
| // frontier and taint state so speculative candidate rejection does not |
| // launder a stale/tainted block into a fresh one. |
| block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE; |
| if (!preserve_block_metadata) { |
| block->flags &= ~IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED; |
| |
| // Copy the death frontier into the block's inline storage. |
| iree_async_frontier_t* block_frontier = |
| iree_hal_memory_tlsf_block_frontier(tlsf, block); |
| if (death_frontier && death_frontier->entry_count > 0) { |
| // Copy entries (clamped to capacity, though the caller should have sized |
| // the frontier to fit). If the death frontier itself exceeds capacity, |
| // we taint immediately. |
| if (death_frontier->entry_count <= tlsf->frontier_capacity) { |
| if (block_frontier != death_frontier) { |
| memcpy(block_frontier, death_frontier, |
| sizeof(iree_async_frontier_t) + |
| (iree_host_size_t)death_frontier->entry_count * |
| sizeof(iree_async_frontier_entry_t)); |
| } |
| } else { |
| // Death frontier too large for inline storage; taint. |
| iree_async_frontier_initialize(block_frontier, 0); |
| block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED; |
| tlsf->tainted_coalesce_count++; |
| } |
| } else { |
| iree_async_frontier_initialize(block_frontier, 0); |
| } |
| } |
| |
| // Coalesce with the right neighbor first (so that the left coalesce can |
| // absorb the combined block). |
| if (!(block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST)) { |
| iree_hal_memory_tlsf_block_index_t right_index = block->next_physical; |
| if (right_index != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_t* right_block = |
| iree_hal_memory_tlsf_block_at(tlsf, right_index); |
| if (right_block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE) { |
| // Remove the right block from its free list. |
| iree_hal_memory_tlsf_remove_free_block(tlsf, right_index); |
| |
| // Merge frontiers: right into this block. |
| iree_hal_memory_tlsf_merge_frontiers( |
| tlsf, block, |
| iree_hal_memory_tlsf_block_frontier(tlsf, right_block)); |
| |
| // Absorb geometry: extend this block to cover the right block. |
| block->length += right_block->length; |
| |
| // Transfer LAST flag. |
| if (right_block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST) { |
| block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST; |
| } |
| |
| // Transfer taint flag. A tainted right neighbor means the merged |
| // block's frontier is untrustworthy; the merge_frontiers call above |
| // will have been a no-op (tainted source has entry_count==0), but the |
| // taint itself must propagate. Zero the frontier to prevent stale data |
| // from being misinterpreted as valid entries. |
| if (right_block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED) { |
| block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED; |
| iree_hal_memory_tlsf_block_frontier(tlsf, block)->entry_count = 0; |
| } |
| |
| // Update physical links. |
| block->next_physical = right_block->next_physical; |
| if (right_block->next_physical != |
| IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_at(tlsf, right_block->next_physical) |
| ->prev_physical = block_index; |
| } |
| |
| // Release the right block node. |
| iree_hal_memory_tlsf_free_node(tlsf, right_index); |
| } |
| } |
| } |
| |
| // Coalesce with the left neighbor. |
| if (block->prev_physical != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_index_t left_index = block->prev_physical; |
| iree_hal_memory_tlsf_block_t* left_block = |
| iree_hal_memory_tlsf_block_at(tlsf, left_index); |
| if (left_block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_FREE) { |
| // Remove the left block from its free list. |
| iree_hal_memory_tlsf_remove_free_block(tlsf, left_index); |
| |
| // Merge frontiers: this block into left. |
| iree_hal_memory_tlsf_merge_frontiers( |
| tlsf, left_block, iree_hal_memory_tlsf_block_frontier(tlsf, block)); |
| |
| // Absorb geometry: extend left to cover this block. |
| left_block->length += block->length; |
| |
| // Transfer LAST flag. |
| if (block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST) { |
| left_block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_LAST; |
| } |
| |
| // Transfer taint flag. As with right coalesce, a tainted source means |
| // the merged block's frontier cannot be trusted; zero it. |
| if (block->flags & IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED) { |
| left_block->flags |= IREE_HAL_MEMORY_TLSF_BLOCK_FLAG_TAINTED; |
| iree_hal_memory_tlsf_block_frontier(tlsf, left_block)->entry_count = 0; |
| } |
| |
| // Update physical links. |
| left_block->next_physical = block->next_physical; |
| if (block->next_physical != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE) { |
| iree_hal_memory_tlsf_block_at(tlsf, block->next_physical) |
| ->prev_physical = left_index; |
| } |
| |
| // Release this block node and switch to inserting the left block. |
| iree_hal_memory_tlsf_free_node(tlsf, block_index); |
| block_index = left_index; |
| block = left_block; |
| } |
| } |
| |
| // Insert the (possibly merged) block into the appropriate free list. |
| iree_hal_memory_tlsf_insert_free_block(tlsf, block_index); |
| } |
| |
| void iree_hal_memory_tlsf_free(iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t block_index, |
| const iree_async_frontier_t* death_frontier) { |
| iree_hal_memory_tlsf_free_impl(tlsf, block_index, death_frontier, |
| /*preserve_block_metadata=*/false); |
| } |
| |
| void iree_hal_memory_tlsf_restore( |
| iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t block_index) { |
| iree_hal_memory_tlsf_free_impl(tlsf, block_index, /*death_frontier=*/NULL, |
| /*preserve_block_metadata=*/true); |
| } |
| |
| void iree_hal_memory_tlsf_query_stats(const iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_stats_t* out_stats) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| IREE_ASSERT_ARGUMENT(out_stats); |
| out_stats->bytes_allocated = tlsf->bytes_allocated; |
| out_stats->bytes_free = tlsf->bytes_free; |
| out_stats->allocation_count = tlsf->allocation_count; |
| out_stats->free_block_count = tlsf->free_block_count; |
| out_stats->tainted_coalesce_count = tlsf->tainted_coalesce_count; |
| } |
| |
| iree_device_size_t iree_hal_memory_tlsf_largest_free_block( |
| const iree_hal_memory_tlsf_t* tlsf) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| if (tlsf->fl_bitmap == 0) return 0; |
| |
| // Find the highest populated FL level. |
| int fl = 63 - iree_math_count_leading_zeros_u64(tlsf->fl_bitmap); |
| IREE_ASSERT(tlsf->sl_bitmaps[fl] != 0); |
| // Find the highest populated SL within that FL level. |
| int sl = 31 - iree_math_count_leading_zeros_u32(tlsf->sl_bitmaps[fl]); |
| // The head block of that list is the largest (or close to it). |
| iree_hal_memory_tlsf_block_index_t head = tlsf->free_lists[fl][sl]; |
| IREE_ASSERT(head != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE); |
| return iree_hal_memory_tlsf_block_at(tlsf, head)->length; |
| } |
| |
| const iree_async_frontier_t* iree_hal_memory_tlsf_block_death_frontier( |
| const iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t block_index) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| IREE_ASSERT(block_index != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE); |
| iree_hal_memory_tlsf_block_t* block = |
| iree_hal_memory_tlsf_block_at(tlsf, block_index); |
| iree_async_frontier_t* frontier = |
| iree_hal_memory_tlsf_block_frontier(tlsf, block); |
| return (frontier->entry_count > 0) ? frontier : NULL; |
| } |
| |
| iree_hal_memory_tlsf_block_flags_t iree_hal_memory_tlsf_block_flags( |
| const iree_hal_memory_tlsf_t* tlsf, |
| iree_hal_memory_tlsf_block_index_t block_index) { |
| IREE_ASSERT_ARGUMENT(tlsf); |
| IREE_ASSERT(block_index != IREE_HAL_MEMORY_TLSF_BLOCK_INDEX_NONE); |
| return iree_hal_memory_tlsf_block_at(tlsf, block_index)->flags; |
| } |