mirror of
https://github.com/odin-lang/Odin.git
synced 2026-08-05 13:48:33 +00:00
1897 lines
71 KiB
Odin
1897 lines
71 KiB
Odin
#+build !js
|
|
#+build !orca
|
|
#+build !wasi
|
|
package runtime
|
|
|
|
import "base:intrinsics"
|
|
|
|
/*
|
|
This is the dynamic heap allocator for the Odin runtime.
|
|
|
|
**Features**
|
|
|
|
- Lock-free guarantee: A thread cannot deadlock or obstruct the allocator;
|
|
some thread makes progress no matter the parallel conditions.
|
|
|
|
- Thread-local heaps: There is no allocator-induced false sharing.
|
|
|
|
- Global storage for unused memory: When a thread finishes cleanly, its memory
|
|
is sent to a global storage where other threads can use it.
|
|
|
|
- Headerless: All bin-sized allocations (any power of two, less than or equal
|
|
to 32KiB, by default) consume no extra space, and as a result of being tightly
|
|
packed, enhance performance with cache locality for programs.
|
|
|
|
|
|
**Terminology**
|
|
|
|
- Bin: an allocation of a fixed size, shared with others of the same size category.
|
|
|
|
- Slab: a fixed-size block within a Superpage that is divided into a constant
|
|
number of Bins at runtime based on the needs of the program.
|
|
|
|
- Sector: a variable-sized bitmap, used to track whether a Bin is free or not.
|
|
|
|
|
|
**Allocation Categories**
|
|
|
|
- Huge: anything in excess of 3/4ths of a Superpage.
|
|
- Slab-wide: anything in excess of the largest Bin size.
|
|
- Bin: anything less than or equal to the largest bin size.
|
|
*/
|
|
|
|
//
|
|
// Tunables
|
|
//
|
|
|
|
// NOTE: Adjusting this constant by itself is rarely enough; `HEAP_SUPERPAGE_CACHE_RATIO`
|
|
// will have to be changed as well.
|
|
HEAP_SLAB_SIZE :: #config(ODIN_HEAP_SLAB_SIZE, 64 * Kilobyte)
|
|
HEAP_MAX_BIN_SIZE :: #config(ODIN_HEAP_MAX_BIN_SIZE, HEAP_SLAB_SIZE / 2)
|
|
HEAP_MIN_BIN_SIZE :: #config(ODIN_HEAP_MIN_BIN_SIZE, 8 * Byte)
|
|
HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES :: #config(ODIN_HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES, 3)
|
|
HEAP_SUPERPAGE_CACHE_RATIO :: #config(ODIN_HEAP_SUPERPAGE_CACHE_RATIO, 20)
|
|
HEAP_PANIC_ON_DOUBLE_FREE :: #config(ODIN_HEAP_PANIC_ON_DOUBLE_FREE, true)
|
|
|
|
//
|
|
// Constants
|
|
//
|
|
|
|
HEAP_MAX_BIN_SHIFT :: intrinsics.constant_log2(HEAP_MAX_BIN_SIZE)
|
|
HEAP_MIN_BIN_SHIFT :: intrinsics.constant_log2(HEAP_MIN_BIN_SIZE)
|
|
HEAP_BIN_RANKS :: 1 + HEAP_MAX_BIN_SHIFT - HEAP_MIN_BIN_SHIFT
|
|
HEAP_HUGE_ALLOCATION_BOOK_KEEPING :: size_of(int) + HEAP_MAX_ALIGNMENT
|
|
HEAP_HUGE_ALLOCATION_THRESHOLD :: SUPERPAGE_SIZE / 4 * 3
|
|
HEAP_MAX_ALIGNMENT :: 64 * Byte
|
|
HEAP_CACHE_SLAB_MAP_STRIDE :: HEAP_SUPERPAGE_CACHE_RATIO * HEAP_SLAB_COUNT
|
|
HEAP_SECTOR_TYPES :: 2 // { local_free, remote_free }
|
|
HEAP_SLAB_ALLOCATION_BOOK_KEEPING :: size_of(Heap_Slab) + HEAP_SECTOR_TYPES * size_of(uint) + HEAP_MAX_ALIGNMENT
|
|
HEAP_SLAB_COUNT :: SUPERPAGE_SIZE / HEAP_SLAB_SIZE - 1
|
|
|
|
@(private="file") INTEGER_BITS :: 8 * size_of(int)
|
|
@(private="file") SECTOR_BITS :: 8 * size_of(uint)
|
|
|
|
//
|
|
// Sanity checking
|
|
//
|
|
|
|
#assert(HEAP_MAX_BIN_SIZE & (HEAP_MAX_BIN_SIZE-1) == 0, "HEAP_MAX_BIN_SIZE must be a power of two.")
|
|
#assert(HEAP_MAX_BIN_SIZE < HEAP_SLAB_SIZE - size_of(Heap_Slab) - HEAP_SECTOR_TYPES * size_of(uint), "HEAP_MAX_BIN_SIZE must be able to fit into one Slab, including its free maps.")
|
|
#assert(HEAP_MAX_BIN_SIZE >= HEAP_MIN_BIN_SIZE, "HEAP_MAX_BIN_SIZE must be greater than or equal to HEAP_MIN_BIN_SIZE.")
|
|
#assert(HEAP_HUGE_ALLOCATION_THRESHOLD <= SUPERPAGE_SIZE - HEAP_HUGE_ALLOCATION_BOOK_KEEPING, "HEAP_HUGE_ALLOCATION_THRESHOLD must be smaller than a Superpage, with enough space for HEAP_HUGE_ALLOCATION_BOOK_KEEPING.")
|
|
#assert(HEAP_MIN_BIN_SIZE & (HEAP_MIN_BIN_SIZE-1) == 0, "HEAP_MIN_BIN_SIZE must be a power of two.")
|
|
#assert(HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES >= 0, "HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES must be positive.")
|
|
#assert(HEAP_MAX_ALIGNMENT & (HEAP_MAX_ALIGNMENT-1) == 0, "HEAP_MAX_ALIGNMENT must be a power of two.")
|
|
#assert(HEAP_SLAB_COUNT > 0, "HEAP_SLAB_COUNT must be greater than zero.")
|
|
#assert(HEAP_SLAB_SIZE & (HEAP_SLAB_SIZE-1) == 0, "HEAP_SLAB_SIZE must be a power of two.")
|
|
#assert(SUPERPAGE_SIZE >= 2 * HEAP_SLAB_SIZE, "SUPERPAGE_SIZE must be at least twice HEAP_SLAB_SIZE.")
|
|
#assert(size_of(Heap_Superpage) < HEAP_SLAB_SIZE, "The Superpage struct must not exceed the size of a Slab.")
|
|
|
|
//
|
|
// Utility Procedures
|
|
//
|
|
|
|
@(require_results, private)
|
|
round_to_nearest_power_of_two :: #force_inline proc "contextless" (n: uint) -> int {
|
|
assert_contextless(n > 1, "This procedure does not handle the edge case of n < 2.")
|
|
return 1 << (INTEGER_BITS - intrinsics.count_leading_zeros(n-1))
|
|
}
|
|
|
|
@(require_results)
|
|
heap_slabs_needed_for_size :: #force_inline proc "contextless" (size: int) -> (result: int) {
|
|
assert_contextless(size > 0)
|
|
assert_contextless(size > HEAP_MAX_BIN_SIZE)
|
|
size := size
|
|
size += HEAP_SLAB_ALLOCATION_BOOK_KEEPING
|
|
result = size / HEAP_SLAB_SIZE + (0 if size % HEAP_SLAB_SIZE == 0 else 1)
|
|
assert_contextless(result > 0)
|
|
assert_contextless(result < HEAP_SLAB_COUNT, "Calculated an overly-large Slab-wide allocation.")
|
|
return
|
|
}
|
|
|
|
@(require_results)
|
|
heap_round_to_bin_size :: #force_inline proc "contextless" (n: int) -> int {
|
|
if n <= HEAP_MIN_BIN_SIZE {
|
|
return HEAP_MIN_BIN_SIZE
|
|
}
|
|
m := round_to_nearest_power_of_two(uint(n))
|
|
assert_contextless(m & (m-1) == 0, "Internal rounding error.")
|
|
return m
|
|
}
|
|
|
|
@(require_results)
|
|
heap_bin_size_to_rank :: #force_inline proc "contextless" (size: int) -> (rank: int) {
|
|
// By this point, a size of zero should've been rounded up to HEAP_MIN_BIN_SIZE.
|
|
assert_contextless(size > 0, "Size must be greater-than zero.")
|
|
assert_contextless(size & (size-1) == 0, "Size must be a power of two.")
|
|
rank = intrinsics.count_trailing_zeros(size >> HEAP_MIN_BIN_SHIFT)
|
|
assert_contextless(rank <= HEAP_BIN_RANKS, "Bin rank calculated incorrectly; it must be less-than-or-equal-to HEAP_BIN_RANKS.")
|
|
return
|
|
}
|
|
|
|
@(require_results)
|
|
find_superpage_from_pointer :: #force_inline proc "contextless" (ptr: rawptr) -> ^Heap_Superpage {
|
|
return cast(^Heap_Superpage)(uintptr(ptr) & ~uintptr(SUPERPAGE_SIZE-1))
|
|
}
|
|
|
|
@(require_results)
|
|
find_slab_from_pointer :: #force_inline proc "contextless" (ptr: rawptr) -> ^Heap_Slab {
|
|
return cast(^Heap_Slab)(uintptr(ptr) & ~uintptr(HEAP_SLAB_SIZE-1))
|
|
}
|
|
|
|
/*
|
|
Get a specific slab by index from a superpage.
|
|
|
|
The position deltas are all constant with respect to the origin, hence this
|
|
procedure should prove faster than accessing an array of pointers.
|
|
*/
|
|
@(require_results)
|
|
heap_superpage_index_slab :: #force_inline proc "contextless" (superpage: ^Heap_Superpage, index: int) -> (slab: ^Heap_Slab) {
|
|
assert_contextless(index >= 0, "The heap allocator tried to index a negative slab index.")
|
|
assert_contextless(index < HEAP_SLAB_COUNT, "The heap allocator tried to index a slab beyond the configured maximum.")
|
|
return cast(^Heap_Slab)(uintptr(superpage) + HEAP_SLAB_SIZE * uintptr(1 + index))
|
|
}
|
|
|
|
//
|
|
// Data Structures
|
|
//
|
|
|
|
/*
|
|
The **Slab** is a fixed-size division of a Superpage, configured at runtime to
|
|
contain fixed-size allocations. It uses two bitmaps to keep track of the
|
|
state of its bins: whether a bin is locally free or remotely free.
|
|
|
|
It is a Slab allocator in its own right, hence the name.
|
|
|
|
Each allocation is self-aligned, up to an alignment size of `HEAP_MAX_ALIGNMENT`
|
|
(64 bytes by default). The slab itself is aligned to its size, allowing
|
|
allocations within to find their owning slab in constant time.
|
|
|
|
Remote threads, in an atomic wait-free manner, flip bits across `remote_free`
|
|
to signal that they have freed memory which does not belong to them. The owning
|
|
thread will then collect those bits when the slab is approaching fullness.
|
|
|
|
|
|
**Fields**:
|
|
|
|
`index` is used to make self-referencing easier. It must be the first field in
|
|
the Slab, as `heap_slab_clear_data` depends on this.
|
|
|
|
`bin_size` tracks the precise byte size of the allocations.
|
|
|
|
`is_dirty` indicates that the slab has been used in the past and its memory may
|
|
not be entirely zeroed.
|
|
|
|
`is_full` is true at some point after `free_bins` == 0 and false otherwise.
|
|
It is atomic and used for inter-thread communication.
|
|
|
|
`has_remote_frees` is true at some point after a bit has been flipped on in
|
|
`remote_free`. It is atomic and used for inter-thread communication.
|
|
|
|
`max_bins` is set at initialization with the number of bins allotted.
|
|
|
|
`free_bins` counts the exact number of free bins known to the allocating
|
|
thread. This value does not yet include any remote free bins.
|
|
|
|
`dirty_bins` tracks which bins have been used and must be zeroed before being
|
|
handed out again. Because we always allocate linearly, this is simply an
|
|
integer indicating the greatest index the slab has ever distributed.
|
|
|
|
`next_free_sector` points to which `uint` in `local_free` contains a free bit.
|
|
It is always the lowest index possible.
|
|
|
|
`sectors` is the length of each bitmap.
|
|
|
|
`local_free` tracks which bins are free.
|
|
|
|
`remote_free` tracks which bins have been freed by other threads.
|
|
It is atomic.
|
|
|
|
`data` points to the first bin and is used for calculating bin positions.
|
|
*/
|
|
Heap_Slab :: struct {
|
|
index: int, // This field must be the first.
|
|
bin_size: int,
|
|
is_dirty: bool,
|
|
|
|
max_bins: int,
|
|
free_bins: int,
|
|
dirty_bins: int,
|
|
next_free_sector: int,
|
|
|
|
is_full: bool, // atomic
|
|
remote_free_bins_scheduled: int, // atomic
|
|
|
|
sectors: int,
|
|
local_free: [^]uint,
|
|
remote_free: [^]uint, // data referenced is atomic
|
|
|
|
// NOTE: `remote_free` and its contents should be fine with regards to
|
|
// alignment for the purposes of atomic access, as every field in a Slab is
|
|
// at least register-sized.
|
|
//
|
|
// Atomically accessing memory that is not aligned to the register size
|
|
// may cause an issue on some systems.
|
|
|
|
data: uintptr,
|
|
|
|
/* ... local_free's data ... */
|
|
/* ... remote_free's data ... */
|
|
/* ... allocation data ... */
|
|
}
|
|
|
|
/*
|
|
The **Superpage** is a single allocation from the operating system's virtual
|
|
memory subsystem, always with a fixed size at compile time, that is used to
|
|
store almost every allocation requested by the program in sub-allocators known
|
|
as Slabs.
|
|
|
|
On almost every platform, this structure will be 2MiB by default.
|
|
|
|
Depending on the operating system, addresses within the space occupied by the
|
|
superpage (and hence its allocations) may also have faster access times due to
|
|
leveraging properties of the Translation Lookaside Buffer.
|
|
|
|
It is always aligned to its size, allowing any address allocated from its space
|
|
to look up the owning superpage in constant-time with bit masking.
|
|
|
|
**Fields:**
|
|
|
|
`huge_size` is precisely how many bytes have been allocated to the address at
|
|
which this structure resides, if it is a Huge allocation. It is zero for normal
|
|
superpages.
|
|
|
|
`prev` points to the previous superpage in the doubly-linked list of all
|
|
superpages for a single thread's heap.
|
|
|
|
`next` points to the next superpage in the same doubly-linked list. It is
|
|
accessed with atomics only when engaging with the orphanage, as that is the
|
|
only time it should change in a parallel situation. For all other cases, the
|
|
thread which owns the superpage is the only one to read this field.
|
|
|
|
`remote_free_set` is true if there might be a slab with remote frees in this superpage.
|
|
|
|
`owner` is the value of `current_thread_id` for the thread which owns this superpage..
|
|
|
|
`master_cache_block` points to the `local_heap_cache` for the thread which owns this
|
|
superpage. This field is used to help synchronize remote freeing.
|
|
|
|
`free_slabs` is the count of slabs which are ready to use for new bin ranks.
|
|
|
|
`next_free_slab_index` is the lowest index of a free slab.
|
|
A value of `HEAP_SLAB_COUNT` means there are no free slabs.
|
|
|
|
`cache_block` is the space where data for the heap's cache is stored, if this
|
|
superpage has been claimed by the heap. It should otherwise be all zero.
|
|
*/
|
|
Heap_Superpage :: struct {
|
|
huge_size: int, // This field must be the first.
|
|
prev: ^Heap_Superpage,
|
|
next: ^Heap_Superpage, // atomic in orphanage, otherwise non-atomic
|
|
|
|
remote_free_set: bool, // atomic
|
|
owner: int, // atomic
|
|
master_cache_block: ^Heap_Cache_Block, // atomic
|
|
|
|
free_slabs: int,
|
|
next_free_slab_index: int,
|
|
|
|
cache_block: Heap_Cache_Block,
|
|
}
|
|
|
|
/*
|
|
`Heap_Cache_Block` is a structure that lives within each `Heap_Superpage` that has been
|
|
claimed by a thread's heap to store heap-relevant metadata for improving
|
|
allocator performance. This structure makes use of space that would have
|
|
otherwise been unused due to the particular alignment needs the allocator has.
|
|
|
|
The number of superpages that each `Heap_Cache_Block` oversees is dictated by the
|
|
`HEAP_SUPERPAGE_CACHE_RATIO` constant. As a thread's heap accumulates more
|
|
superpages, the heap will need to claim additional superpages - at the rate of
|
|
that constant - to keep track of all the possible data.
|
|
|
|
The arrays are configured in such a manner that they can never overflow, and
|
|
the heap will always have more than enough space to record the data needed.
|
|
|
|
|
|
The `HEAP_CACHE_SLAB_MAP_STRIDE` is of note, as it must contain the number of slabs
|
|
per superpage (31, by default) times the number of superpages overseen per
|
|
`Heap_Cache_Block`. This is largely where all the space is allocated, but it pays off
|
|
handsomely in allowing the allocator to find if a slab is available for any
|
|
particular bin size in constant time.
|
|
|
|
In practice, this is an excessive amount of space allocated for a map of arrays
|
|
for this purpose, but it is entirely possible that the allocator could get into
|
|
a spot where this hash map could overflow if it used any less space. For
|
|
instance, if the allocator had as many superpages as possible for one
|
|
`Heap_Cache_Block`, filled all the slabs, then the program freed one bin from each
|
|
slab, the allocator would need every slot in the map. The cache has as much
|
|
space to prevent just that problem from arising.
|
|
|
|
**Fields:**
|
|
|
|
`in_use` will be true, if the parent `Heap_Superpage` is using the space occupied by
|
|
this struct. This indicates that the superpage should not be freed.
|
|
|
|
`next_cache_block` is an atomic pointer to the next `Heap_Cache_Block`. Each
|
|
`Heap_Cache_Block` is linked together in this way to allow each heap to expand
|
|
its available space for tracking information.
|
|
|
|
|
|
_The next three fields are only used in the `Heap_Superpage` pointed to by `local_heap`._
|
|
|
|
`length` is how many `Heap_Cache_Block` structs are in use across the local
|
|
thread's heap.
|
|
|
|
`owned_superpages` is how many superpages are in use by the local thread's
|
|
heap.
|
|
|
|
`remote_free_count` is an estimate of how many superpages have slabs with
|
|
remote frees available to merge.
|
|
|
|
|
|
_The next three fields constitute the main data used in this struct, and each
|
|
of them are like partitions, spread across a linked list._
|
|
|
|
`slab_map` is a hash map of arrays, keyed by bin rank, used to quickly find a
|
|
slab with free bins for a particular bin size. It uses linear probing.
|
|
|
|
`superpages_with_free_slabs` is an array of superpages with `free_slabs`
|
|
greater than zero, used for quickly allocating new slabs when none can be found
|
|
in `slab_map`.
|
|
|
|
`superpages_with_remote_frees` is an atomic set containing references to
|
|
superpages that may have slabs with remotely free bins. Superpages are only
|
|
added if the slab was full at the time, and an invasive flag keeps the
|
|
algorithm from having to search the entire span for an existence check.
|
|
|
|
Keep in mind that even though a superpage is added to this set when a slab is
|
|
full and freed remotely, that state need not persist; the owning thread can
|
|
free a bin before the remote frees are merged, invalidating that part of the
|
|
heuristic.
|
|
*/
|
|
Heap_Cache_Block :: struct {
|
|
in_use: bool,
|
|
next_cache_block: ^Heap_Cache_Block, // atomic
|
|
|
|
// { only used in `local_heap`
|
|
length: int,
|
|
owned_superpages: int,
|
|
remote_free_count: int, // atomic
|
|
// }
|
|
|
|
slab_map: [HEAP_CACHE_SLAB_MAP_STRIDE*HEAP_BIN_RANKS]^Heap_Slab,
|
|
superpages_with_free_slabs: [HEAP_SUPERPAGE_CACHE_RATIO]^Heap_Superpage,
|
|
superpages_with_remote_frees: [HEAP_SUPERPAGE_CACHE_RATIO]^Heap_Superpage, // atomic
|
|
}
|
|
|
|
//
|
|
// Superstructure Allocation
|
|
//
|
|
|
|
/*
|
|
Allocate memory for a Superpage from the operating system and do any initialization work.
|
|
*/
|
|
@(require_results)
|
|
heap_make_superpage :: proc "contextless" () -> (superpage: ^Heap_Superpage) {
|
|
superpage = cast(^Heap_Superpage)allocate_virtual_memory_superpage()
|
|
assert_contextless(uintptr(superpage) & uintptr(SUPERPAGE_SIZE-1) == 0, "The operating system returned virtual memory which isn't aligned to a Superpage-sized boundary.")
|
|
|
|
superpage.owner = get_current_thread_id()
|
|
superpage.free_slabs = HEAP_SLAB_COUNT
|
|
|
|
// Each Slab is aligned to its size, so that finding which slab an
|
|
// allocated pointer is assigned to is a constant operation by bit masking.
|
|
//
|
|
// However, this means we must waste one Slab per Superpage so that the
|
|
// Superpage data can live nearby inside the chunk of virtual memory.
|
|
base := uintptr(superpage) + HEAP_SLAB_SIZE
|
|
for i in 0..<HEAP_SLAB_COUNT {
|
|
slab := heap_superpage_index_slab(superpage, i)
|
|
slab.index = i
|
|
assert_contextless(find_superpage_from_pointer(slab) == superpage, "Lookup of slab to superpage failed.")
|
|
assert_contextless(base == uintptr(find_slab_from_pointer(slab)), "Reverse lookup from slab base pointer failed.")
|
|
assert_contextless(base - uintptr(superpage) + HEAP_SLAB_SIZE - 1 < SUPERPAGE_SIZE, "A slab was setup beyond the superpage boundary.")
|
|
base += HEAP_SLAB_SIZE
|
|
}
|
|
|
|
return superpage
|
|
}
|
|
|
|
/*
|
|
Give the Superpage back to the operating system if the orphanage has more than
|
|
`HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES` superpages.
|
|
|
|
Otherwise, place it in the orphanage for another thread to adopt later.
|
|
*/
|
|
heap_free_superpage :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
// We cannot free a Superpage that is being used by the heap cache.
|
|
// It will stay available for future allocations.
|
|
assert_contextless(superpage.free_slabs == HEAP_SLAB_COUNT, "The heap allocator tried to free a superpage that was not fully free.")
|
|
assert_contextless(!superpage.cache_block.in_use, "The heap allocator tried to free a superpage that's in use by its cache.")
|
|
heap_unlink_superpage(superpage)
|
|
heap_cache_remove_superpage_with_free_slabs(superpage)
|
|
heap_cache_unregister_superpage(superpage)
|
|
// We only return excess Superpages to the operating system.
|
|
//
|
|
// The rest are put to a global orphanage so that the memory is ready to be
|
|
// used as soon as needed.
|
|
if intrinsics.atomic_add_explicit(&heap_orphanage_count, 1, .Acq_Rel) >= HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES {
|
|
intrinsics.atomic_sub_explicit(&heap_orphanage_count, 1, .Relaxed)
|
|
free_virtual_memory(superpage, SUPERPAGE_SIZE)
|
|
heap_debug_cover(.Superpage_Freed_On_Full_Orphanage)
|
|
} else {
|
|
heap_push_orphan(superpage)
|
|
heap_debug_cover(.Superpage_Pushed_To_Orphanage)
|
|
}
|
|
}
|
|
|
|
/*
|
|
Remove a superpage from a heap's cache of superpages.
|
|
*/
|
|
heap_cache_unregister_superpage :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
if superpage.cache_block.in_use {
|
|
return
|
|
}
|
|
local_heap_cache.owned_superpages -= 1
|
|
intrinsics.atomic_store_explicit(&superpage.master_cache_block, nil, .Release)
|
|
heap_debug_cover(.Superpage_Unregistered)
|
|
}
|
|
|
|
/*
|
|
Add a superpage to a heap's cache of superpages.
|
|
|
|
This will take note if the superpage has free slabs, what are the contents of
|
|
its slabs, and it will merge any waiting remote free bins.
|
|
*/
|
|
heap_cache_register_superpage :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
// Expand the heap cache's available space if needed.
|
|
local_heap_cache.owned_superpages += 1
|
|
if local_heap_cache.owned_superpages / HEAP_SUPERPAGE_CACHE_RATIO > local_heap_cache.length {
|
|
tail := local_heap_cache
|
|
for /**/; tail.next_cache_block != nil; tail = tail.next_cache_block { }
|
|
heap_cache_expand(tail)
|
|
}
|
|
|
|
// This must come after expansion to prevent a remote thread from running out of space.
|
|
// The cache is first expanded, _then_ other threads may know about it.
|
|
intrinsics.atomic_store_explicit(&superpage.master_cache_block, local_heap_cache, .Release)
|
|
|
|
// Register the superpage for allocation.
|
|
if superpage.free_slabs > 0 {
|
|
// Register the superpage as having free slabs available.
|
|
heap_cache_add_superpage_with_free_slabs(superpage)
|
|
heap_debug_cover(.Superpage_Registered_With_Free_Slabs)
|
|
}
|
|
|
|
// Register slabs.
|
|
if superpage.free_slabs < HEAP_SLAB_COUNT {
|
|
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 {
|
|
i += 1
|
|
if slab.bin_size == 0 {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// When adopting a new Superpage, we take the opportunity to
|
|
// merge any remote frees. This is important because it's
|
|
// possible for another thread to remotely free memory while
|
|
// the thread which owned it is in limbo.
|
|
if intrinsics.atomic_load_explicit(&slab.remote_free_bins_scheduled, .Acquire) > 0 {
|
|
heap_merge_remote_frees(slab)
|
|
|
|
if slab.free_bins > 0 {
|
|
// Synchronize with any thread that might be trying to
|
|
// free as we merge.
|
|
intrinsics.atomic_store_explicit(&slab.is_full, false, .Release)
|
|
}
|
|
}
|
|
|
|
// Free any empty Slabs and register the ones with free bins.
|
|
if slab.free_bins == slab.max_bins {
|
|
if slab.bin_size > HEAP_MAX_BIN_SIZE {
|
|
heap_free_wide_slab(superpage, slab)
|
|
} else {
|
|
heap_free_slab(superpage, slab)
|
|
}
|
|
} else if slab.bin_size <= HEAP_MAX_BIN_SIZE && slab.free_bins > 0 {
|
|
heap_cache_add_slab(slab, heap_bin_size_to_rank(slab.bin_size))
|
|
}
|
|
|
|
heap_debug_cover(.Superpage_Registered_With_Slab_In_Use)
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Get a superpage, first by adopting any orphan, or allocating memory from the
|
|
operating system if one is not available.
|
|
*/
|
|
@(require_results)
|
|
heap_get_superpage:: proc "contextless" () -> (superpage: ^Heap_Superpage) {
|
|
superpage = heap_pop_orphan()
|
|
if superpage == nil {
|
|
superpage = heap_make_superpage()
|
|
heap_debug_cover(.Superpage_Created_By_Empty_Orphanage)
|
|
} else {
|
|
heap_debug_cover(.Superpage_Adopted_From_Orphanage)
|
|
}
|
|
assert_contextless(superpage != nil, "The heap allocator failed to get a superpage.")
|
|
return
|
|
}
|
|
|
|
/*
|
|
Make an allocation in the Huge category.
|
|
*/
|
|
@(require_results)
|
|
heap_make_huge_allocation :: proc "contextless" (size: int) -> (ptr: rawptr) {
|
|
// NOTE: ThreadSanitizer may wrongly say that this is the source of a data
|
|
// race. This is because a virtual memory address has been allocated once,
|
|
// returned to the operating system, then given back to the process in a
|
|
// different thread.
|
|
//
|
|
// It is otherwise impossible for us to race on newly allocated memory.
|
|
size := size
|
|
if size < SUPERPAGE_SIZE - HEAP_HUGE_ALLOCATION_BOOK_KEEPING {
|
|
size = SUPERPAGE_SIZE
|
|
heap_debug_cover(.Huge_Alloc_Size_Set_To_Superpage)
|
|
} else {
|
|
size += HEAP_HUGE_ALLOCATION_BOOK_KEEPING
|
|
heap_debug_cover(.Huge_Alloc_Size_Adjusted)
|
|
}
|
|
assert_contextless(size >= SUPERPAGE_SIZE, "Calculated incorrect Huge allocation size.")
|
|
|
|
// All free operations assume every pointer has a Superpage at the
|
|
// Superpage boundary of the pointer, and a Huge allocation is no
|
|
// different.
|
|
//
|
|
// The size of the allocation is written as an integer at the beginning,
|
|
// but all other fields are left zero-initialized. We then align forward to
|
|
// `HEAP_MAX_ALIGNMENT` (64 bytes, by default) and give that to the user.
|
|
superpage := cast(^Heap_Superpage)allocate_virtual_memory_aligned(size, SUPERPAGE_SIZE)
|
|
assert_contextless(uintptr(superpage) & uintptr(SUPERPAGE_SIZE-1) == 0, "The operating system returned virtual memory which isn't aligned to a Superpage-sized boundary.")
|
|
|
|
superpage.huge_size = size
|
|
|
|
u := uintptr(superpage) + HEAP_HUGE_ALLOCATION_BOOK_KEEPING
|
|
ptr = rawptr(u - u & (HEAP_MAX_ALIGNMENT-1))
|
|
|
|
assert_contextless(uintptr(ptr) & (HEAP_MAX_ALIGNMENT-1) == 0, "Huge allocation is not aligned to HEAP_MAX_ALIGNMENT.")
|
|
assert_contextless(find_superpage_from_pointer(ptr) == superpage, "Huge allocation reverse lookup failed.")
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
Make an allocation that is at least one entire Slab wide from the provided superpage.
|
|
|
|
This will return false if the Superpage lacks enough contiguous Slabs to fit the size.
|
|
*/
|
|
@(require_results)
|
|
heap_make_slab_sized_allocation :: proc "contextless" (superpage: ^Heap_Superpage, size: int) -> (ptr: rawptr, ok: bool) {
|
|
assert_contextless(0 <= superpage.next_free_slab_index && superpage.next_free_slab_index < HEAP_SLAB_COUNT, "Invalid next_free_slab_index.")
|
|
contiguous := heap_slabs_needed_for_size(size)
|
|
|
|
find_run: for start := superpage.next_free_slab_index; start < HEAP_SLAB_COUNT-contiguous+1; /**/ {
|
|
n := contiguous
|
|
|
|
for i := start; i < HEAP_SLAB_COUNT; /**/ {
|
|
bin_size := heap_superpage_index_slab(superpage, i).bin_size
|
|
if bin_size > HEAP_MAX_BIN_SIZE {
|
|
// Skip contiguous slabs.
|
|
start = i + heap_slabs_needed_for_size(bin_size)
|
|
continue find_run
|
|
} else if bin_size != 0 {
|
|
start = i + 1
|
|
continue find_run
|
|
}
|
|
n -= 1
|
|
if n > 0 {
|
|
i += 1
|
|
continue
|
|
}
|
|
// Setup the Slab header.
|
|
// This will be a single-sector Slab that may span several Slabs.
|
|
slab := heap_superpage_index_slab(superpage, start)
|
|
|
|
// Setup slab.
|
|
if slab.is_dirty {
|
|
heap_slab_clear_data(slab)
|
|
}
|
|
slab.bin_size = size
|
|
slab.is_full = true
|
|
slab.max_bins = 1
|
|
slab.dirty_bins = 1
|
|
slab.sectors = 1
|
|
slab.local_free = cast([^]uint)(uintptr(slab) + size_of(Heap_Slab))
|
|
slab.remote_free = cast([^]uint)(uintptr(slab) + size_of(Heap_Slab) + 1 * size_of(uint))
|
|
data := uintptr(slab) + HEAP_SLAB_ALLOCATION_BOOK_KEEPING
|
|
ptr = rawptr(data - data & (HEAP_MAX_ALIGNMENT-1))
|
|
slab.data = uintptr(ptr)
|
|
assert_contextless(uintptr(ptr) & (HEAP_MAX_ALIGNMENT-1) == 0, "Slab-wide allocation's data pointer is not correctly aligned.")
|
|
assert_contextless(int(uintptr(ptr) - uintptr(superpage)) + size < SUPERPAGE_SIZE, "Incorrectly calculated Slab-wide allocation exceeds Superpage end boundary.")
|
|
|
|
// Wipe any non-zero data from slabs ahead of the header.
|
|
for x in start+1..=i {
|
|
next_slab := heap_superpage_index_slab(superpage, x)
|
|
next_slab.index = 0
|
|
if next_slab.is_dirty {
|
|
heap_slab_clear_data(next_slab)
|
|
}
|
|
}
|
|
|
|
// Update statistics.
|
|
superpage.free_slabs -= contiguous
|
|
assert_contextless(superpage.free_slabs >= 0, "The heap allocator caused a superpage's free_slabs to go negative.")
|
|
if superpage.free_slabs == 0 {
|
|
heap_cache_remove_superpage_with_free_slabs(superpage)
|
|
}
|
|
// NOTE: Start from zero again, because we may have skipped a non-contiguous block.
|
|
heap_update_next_free_slab_index(superpage, 0)
|
|
|
|
return ptr, true
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
//
|
|
// Slabs
|
|
//
|
|
|
|
/*
|
|
Do everything that is needed to ready a Slab for a specific bin size.
|
|
|
|
This involves a handful of space calculations and writing the header.
|
|
*/
|
|
@(require_results)
|
|
heap_slab_setup :: proc "contextless" (superpage: ^Heap_Superpage, rounded_size: int) -> (slab: ^Heap_Slab) {
|
|
assert_contextless(0 <= superpage.next_free_slab_index && superpage.next_free_slab_index < HEAP_SLAB_COUNT, "The heap allocator found a Superpage with an invalid next_free_slab_index.")
|
|
assert_contextless(superpage.free_slabs > 0, "The heap allocator tried to setup a Slab in an exhausted Superpage.")
|
|
|
|
superpage.free_slabs -= 1
|
|
assert_contextless(superpage.free_slabs >= 0, "The heap allocator caused a Superpage's free_slabs to go negative.")
|
|
|
|
slab = heap_superpage_index_slab(superpage, superpage.next_free_slab_index)
|
|
if slab.is_dirty {
|
|
heap_slab_clear_data(slab)
|
|
}
|
|
slab.bin_size = rounded_size
|
|
|
|
// The book-keeping structures compete for the same space as the data,
|
|
// so we have to go back and forth with the math a bit.
|
|
bins := HEAP_SLAB_SIZE / rounded_size
|
|
sectors := bins / INTEGER_BITS
|
|
sectors += 0 if bins % INTEGER_BITS == 0 else 1
|
|
|
|
// We'll waste `2 * HEAP_MAX_ALIGNMENT` bytes per slab to simplify the math
|
|
// behind getting the pointer to the first bin to align properly.
|
|
// Otherwise we'd have to go back and forth even more.
|
|
bookkeeping_bin_cost := max(1, int(size_of(Heap_Slab) + HEAP_SECTOR_TYPES * uintptr(sectors) * size_of(uint) + 2 * HEAP_MAX_ALIGNMENT) / rounded_size)
|
|
bins -= bookkeeping_bin_cost
|
|
sectors = bins / INTEGER_BITS
|
|
sectors += 0 if bins % INTEGER_BITS == 0 else 1
|
|
|
|
slab.sectors = sectors
|
|
slab.free_bins = bins
|
|
slab.max_bins = bins
|
|
|
|
base_alignment := uintptr(min(HEAP_MAX_ALIGNMENT, rounded_size))
|
|
|
|
slab_bitmap_base := uintptr(slab) + size_of(Heap_Slab)
|
|
total_byte_size_of_all_bitmaps := HEAP_SECTOR_TYPES * uintptr(sectors) * size_of(uint)
|
|
pointer_padding := (uintptr(base_alignment) - (slab_bitmap_base + total_byte_size_of_all_bitmaps)) & uintptr(base_alignment - 1)
|
|
|
|
// These bitmaps are placed at the end of the struct, one after the other.
|
|
slab.local_free = cast([^]uint)(slab_bitmap_base)
|
|
slab.remote_free = cast([^]uint)(slab_bitmap_base + uintptr(sectors) * size_of(uint))
|
|
// This pointer is specifically aligned.
|
|
slab.data = (slab_bitmap_base + total_byte_size_of_all_bitmaps + pointer_padding)
|
|
|
|
assert_contextless(slab.data & (base_alignment-1) == 0, "Incorrect calculation for aligning Slab data pointer.")
|
|
assert_contextless(size_of(Heap_Slab) + int(total_byte_size_of_all_bitmaps) + bins * rounded_size < HEAP_SLAB_SIZE, "Slab internal allocation overlimit.")
|
|
assert_contextless(find_slab_from_pointer(rawptr(slab.data)) == slab, "Slab data pointer cannot be traced back to its Slab.")
|
|
|
|
// Set all of the local free bits.
|
|
{
|
|
full_sectors := bins / INTEGER_BITS
|
|
partial_sector := bins % INTEGER_BITS
|
|
for i in 0..<full_sectors {
|
|
slab.local_free[i] = max(uint)
|
|
}
|
|
if partial_sector > 0 {
|
|
slab.local_free[sectors-1] = (1 << uint(bins % INTEGER_BITS)) - 1
|
|
heap_debug_cover(.Slab_Adjusted_For_Partial_Sector)
|
|
}
|
|
}
|
|
|
|
// Update the next free slab.
|
|
heap_update_next_free_slab_index(superpage, superpage.next_free_slab_index + 1)
|
|
|
|
// Make the slab known to the heap.
|
|
heap_cache_add_slab(slab, heap_bin_size_to_rank(rounded_size))
|
|
|
|
if superpage.free_slabs == 0 {
|
|
heap_cache_remove_superpage_with_free_slabs(superpage)
|
|
heap_debug_cover(.Superpage_Removed_From_Open_Cache_By_Slab)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
Set everything after the index field to zero in a Slab.
|
|
*/
|
|
heap_slab_clear_data :: proc "contextless" (slab: ^Heap_Slab) {
|
|
intrinsics.mem_zero_volatile(rawptr(uintptr(slab) + size_of(int)), HEAP_SLAB_SIZE - size_of(int))
|
|
}
|
|
|
|
/*
|
|
Mark a Slab-wide allocation as no longer in use by the Superpage.
|
|
*/
|
|
heap_free_wide_slab :: proc "contextless" (superpage: ^Heap_Superpage, slab: ^Heap_Slab) {
|
|
assert_contextless(slab.bin_size > HEAP_MAX_BIN_SIZE, "The heap allocator tried to wide-free a non-wide slab.")
|
|
// There is only one bit for a Slab-wide allocation, so this will be easy.
|
|
contiguous := heap_slabs_needed_for_size(slab.bin_size)
|
|
previously_full_superpage := superpage.free_slabs == 0
|
|
superpage.free_slabs += contiguous
|
|
|
|
for i in slab.index..<slab.index+contiguous {
|
|
// Rewrite the index into place in case it was overwritten, then mark
|
|
// all slabs of the allocation as dirty and available.
|
|
next_slab := heap_superpage_index_slab(superpage, i)
|
|
next_slab.index = i
|
|
next_slab.is_dirty = true
|
|
next_slab.bin_size = 0
|
|
}
|
|
|
|
superpage.next_free_slab_index = min(superpage.next_free_slab_index, slab.index)
|
|
if superpage.free_slabs == HEAP_SLAB_COUNT && !superpage.cache_block.in_use {
|
|
heap_free_superpage(superpage)
|
|
heap_debug_cover(.Superpage_Freed_By_Wide_Slab)
|
|
return
|
|
}
|
|
if previously_full_superpage {
|
|
heap_cache_add_superpage_with_free_slabs(superpage)
|
|
heap_debug_cover(.Superpage_Added_To_Open_Cache_By_Freeing_Wide_Slab)
|
|
}
|
|
}
|
|
|
|
/*
|
|
Mark a Slab as no longer in use by the Superpage.
|
|
*/
|
|
heap_free_slab :: proc "contextless" (superpage: ^Heap_Superpage, slab: ^Heap_Slab) {
|
|
if superpage.free_slabs == 0 {
|
|
heap_cache_add_superpage_with_free_slabs(superpage)
|
|
heap_debug_cover(.Superpage_Added_To_Open_Cache_By_Slab)
|
|
}
|
|
slab.is_dirty = true
|
|
slab.bin_size = 0
|
|
superpage.free_slabs += 1
|
|
assert_contextless(superpage.free_slabs <= HEAP_SLAB_COUNT)
|
|
superpage.next_free_slab_index = min(superpage.next_free_slab_index, slab.index)
|
|
}
|
|
|
|
/*
|
|
Merge bins marked as free by remote threads.
|
|
|
|
During normal operation, this is only called:
|
|
|
|
1. when a slab is about to become full during allocation and there are known
|
|
remote frees.
|
|
2. when a slab is known to be full and a remote thread has freed a bin.
|
|
3. when a superpage is adopted and slabs within are known to have remote frees.
|
|
4. when a superpage is being released to the orphanage after clean exit of a
|
|
thread, and one of its slabs is known to have remote frees.
|
|
|
|
This procedure returns an estimation of the number of remote free bins left to
|
|
merge which were not captured in this merge.
|
|
*/
|
|
heap_merge_remote_frees :: proc "contextless" (slab: ^Heap_Slab) -> (bins_left: int) {
|
|
assert_contextless(slab.bin_size > 0, "The heap allocator tried to merge remote frees on an unused slab.")
|
|
|
|
merged_bins := 0
|
|
next_free_sector := slab.next_free_sector
|
|
|
|
// Atomically merge in all of the bits set by other threads.
|
|
for i in 0..<slab.sectors {
|
|
remote_free_sector_bits := intrinsics.atomic_exchange_explicit(&slab.remote_free[i], 0, .Acq_Rel)
|
|
if remote_free_sector_bits == 0 {
|
|
continue
|
|
}
|
|
merged_bins += int(intrinsics.count_ones(remote_free_sector_bits))
|
|
slab.local_free[i] |= remote_free_sector_bits
|
|
next_free_sector = min(next_free_sector, i)
|
|
}
|
|
|
|
bins_left = intrinsics.atomic_sub_explicit(&slab.remote_free_bins_scheduled, merged_bins, .Seq_Cst) - merged_bins
|
|
|
|
slab.free_bins += merged_bins
|
|
slab.next_free_sector = next_free_sector
|
|
heap_debug_cover(.Merged_Remote_Frees)
|
|
return
|
|
}
|
|
|
|
|
|
//
|
|
// Superpages
|
|
//
|
|
|
|
/*
|
|
Update the cached index for the next available Slab.
|
|
|
|
This will save a little time on the next allocation that needs one.
|
|
*/
|
|
heap_update_next_free_slab_index :: proc "contextless" (superpage: ^Heap_Superpage, start_at: int) {
|
|
// Reset it to the "no free slab" state.
|
|
superpage.next_free_slab_index = HEAP_SLAB_COUNT
|
|
|
|
if superpage.free_slabs == 0 {
|
|
heap_debug_cover(.Superpage_Updated_Next_Free_Slab_Index_As_Empty)
|
|
return
|
|
}
|
|
|
|
// Find a free slab.
|
|
for i := start_at; i < HEAP_SLAB_COUNT; /**/ {
|
|
bin_size := heap_superpage_index_slab(superpage, i).bin_size
|
|
if bin_size == 0 {
|
|
superpage.next_free_slab_index = i
|
|
heap_debug_cover(.Superpage_Updated_Next_Free_Slab_Index)
|
|
return
|
|
} else if bin_size > HEAP_MAX_BIN_SIZE {
|
|
// Skip contiguous slabs.
|
|
i += heap_slabs_needed_for_size(bin_size)
|
|
} else {
|
|
i += 1
|
|
}
|
|
}
|
|
|
|
panic_contextless("The heap allocator was unable to find a free slab in a superpage with free_slabs > 0.")
|
|
}
|
|
|
|
//
|
|
// The Heap Cache
|
|
//
|
|
|
|
/*
|
|
Link a new superpage into the heap.
|
|
*/
|
|
heap_link_superpage :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
assert_contextless(superpage.prev == nil, "The heap allocator tried to link an already-linked superpage.")
|
|
superpage.prev = local_heap_tail
|
|
local_heap_tail.next = superpage
|
|
local_heap_tail = superpage
|
|
heap_debug_cover(.Superpage_Linked)
|
|
}
|
|
|
|
/*
|
|
Unlink a superpage from the heap.
|
|
|
|
There must always be at least one superpage in the heap after the first
|
|
non-Huge allocation, in order to track heap metadata.
|
|
*/
|
|
heap_unlink_superpage :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
if superpage == local_heap_tail {
|
|
assert_contextless(superpage.next == nil, "The heap allocator's tail superpage has a next link.")
|
|
assert_contextless(superpage.prev != nil, "The heap allocator's tail superpage has no previous link.")
|
|
local_heap_tail = superpage.prev
|
|
// We never unlink all superpages, so no need to check validity here.
|
|
superpage.prev.next = nil
|
|
heap_debug_cover(.Superpage_Unlinked_Tail)
|
|
return
|
|
}
|
|
if superpage.prev != nil {
|
|
superpage.prev.next = superpage.next
|
|
}
|
|
if superpage.next != nil {
|
|
superpage.next.prev = superpage.prev
|
|
}
|
|
heap_debug_cover(.Superpage_Unlinked_Non_Tail)
|
|
}
|
|
|
|
/*
|
|
Mark a superpage from another thread as having had a bin remotely freed.
|
|
*/
|
|
heap_remote_cache_add_remote_free_superpage :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
if intrinsics.atomic_exchange_explicit(&superpage.remote_free_set, true, .Acq_Rel) {
|
|
// Already set.
|
|
heap_debug_cover(.Superpage_Add_Remote_Free_Guarded_With_Set)
|
|
return
|
|
}
|
|
master := intrinsics.atomic_load_explicit(&superpage.master_cache_block, .Acquire)
|
|
if master == nil {
|
|
// This superpage is not owned by anyone.
|
|
// Its remote frees will be acknowledged in whole when it's adopted.
|
|
heap_debug_cover(.Superpage_Add_Remote_Free_Guarded_With_Masterless)
|
|
return
|
|
}
|
|
defer heap_debug_cover(.Superpage_Added_Remote_Free)
|
|
|
|
cache := master
|
|
for {
|
|
for i := 0; i < len(cache.superpages_with_remote_frees); i += 1 {
|
|
old, swapped := intrinsics.atomic_compare_exchange_strong_explicit(&cache.superpages_with_remote_frees[i], nil, superpage, .Acq_Rel, .Relaxed)
|
|
assert_contextless(old != superpage, "A remote thread found a duplicate of a superpage in a heap's superpages_with_remote_frees.")
|
|
if swapped {
|
|
intrinsics.atomic_add_explicit(&master.remote_free_count, 1, .Release)
|
|
return
|
|
}
|
|
}
|
|
next_cache_block := intrinsics.atomic_load_explicit(&cache.next_cache_block, .Acquire)
|
|
assert_contextless(next_cache_block != nil, "A remote thread failed to find free space for a new entry in another heap's superpages_with_remote_frees.")
|
|
cache = next_cache_block
|
|
}
|
|
}
|
|
|
|
/*
|
|
Claim an additional superpage for the heap cache.
|
|
*/
|
|
heap_cache_expand :: proc "contextless" (cache: ^Heap_Cache_Block) {
|
|
superpage := find_superpage_from_pointer(cache)
|
|
assert_contextless(superpage.next != nil, "The heap allocator tried to expand its cache but has run out of linked superpages.")
|
|
new_next_cache_block := &(superpage.next).cache_block
|
|
assert_contextless(new_next_cache_block.in_use == false, "The heap allocator tried to expand its cache, but the candidate superpage is already using its cache block.")
|
|
new_next_cache_block.in_use = true
|
|
intrinsics.atomic_store_explicit(&cache.next_cache_block, new_next_cache_block, .Release)
|
|
local_heap_cache.length += 1
|
|
heap_debug_cover(.Heap_Expanded_Cache_Data)
|
|
}
|
|
|
|
/*
|
|
Add a slab to the heap's cache, keyed to the bin size rank of `rank`.
|
|
*/
|
|
heap_cache_add_slab :: proc "contextless" (slab: ^Heap_Slab, rank: int) {
|
|
assert_contextless(slab != nil)
|
|
cache := local_heap_cache
|
|
for {
|
|
start := rank * HEAP_CACHE_SLAB_MAP_STRIDE
|
|
for i := start; i < start+HEAP_CACHE_SLAB_MAP_STRIDE; i += 1 {
|
|
assert_contextless(cache.slab_map[i] != slab, "The heap allocator found a duplicate entry in its slab map.")
|
|
if cache.slab_map[i] == nil {
|
|
cache.slab_map[i] = slab
|
|
return
|
|
}
|
|
}
|
|
assert_contextless(cache.next_cache_block != nil)
|
|
cache = cache.next_cache_block
|
|
}
|
|
}
|
|
|
|
/*
|
|
Get a slab from the heap's cache for an allocation of `rounded_size` bytes.
|
|
|
|
If no slabs are already available for the bin rank, one will be created.
|
|
*/
|
|
@(require_results)
|
|
heap_cache_get_slab :: proc "contextless" (rounded_size: int) -> (slab: ^Heap_Slab) {
|
|
rank := heap_bin_size_to_rank(rounded_size)
|
|
slab = local_heap_cache.slab_map[rank * HEAP_CACHE_SLAB_MAP_STRIDE]
|
|
if slab == nil {
|
|
superpage := local_heap_cache.superpages_with_free_slabs[0]
|
|
if superpage == nil {
|
|
superpage = heap_get_superpage()
|
|
heap_link_superpage(superpage)
|
|
heap_cache_register_superpage(superpage)
|
|
}
|
|
assert_contextless(superpage.free_slabs > 0)
|
|
slab = heap_slab_setup(superpage, rounded_size)
|
|
}
|
|
return
|
|
}
|
|
|
|
/*
|
|
Remove a slab with the corresponding bin rank from the heap's cache.
|
|
*/
|
|
heap_cache_remove_slab :: proc "contextless" (slab: ^Heap_Slab, rank: int) {
|
|
cache := local_heap_cache
|
|
assert_contextless(cache.in_use)
|
|
assert_contextless(slab.bin_size == 1 << (HEAP_MIN_BIN_SHIFT + uint(rank)))
|
|
for {
|
|
assert_contextless(cache != nil)
|
|
start := rank * HEAP_CACHE_SLAB_MAP_STRIDE
|
|
for i := start; i < start+HEAP_CACHE_SLAB_MAP_STRIDE; i += 1 {
|
|
if cache.slab_map[i] == slab {
|
|
// Swap with the tail.
|
|
source_i := i
|
|
target_cache := cache
|
|
i += 1
|
|
for {
|
|
for j := i; j < start+HEAP_CACHE_SLAB_MAP_STRIDE; j += 1 {
|
|
if target_cache.slab_map[j] == nil {
|
|
cache.slab_map[source_i] = target_cache.slab_map[j-1]
|
|
target_cache.slab_map[j-1] = nil
|
|
return
|
|
}
|
|
}
|
|
if target_cache.next_cache_block == nil {
|
|
// The entry is at the end of the stride and we have
|
|
// run out of space to search.
|
|
cache.slab_map[source_i] = nil
|
|
assert_contextless(source_i % (HEAP_CACHE_SLAB_MAP_STRIDE-1) == 0, "The heap allocator tried to remove a non-terminal slab map entry when it should have swapped it with the tail.")
|
|
return
|
|
} else if target_cache.next_cache_block.slab_map[start] == nil {
|
|
// The starting bucket in the next cache block is empty,
|
|
// so we terminate on the current stride's final bucket.
|
|
cache.slab_map[source_i] = target_cache.slab_map[start+HEAP_CACHE_SLAB_MAP_STRIDE-1]
|
|
target_cache.slab_map[start+HEAP_CACHE_SLAB_MAP_STRIDE-1] = nil
|
|
return
|
|
}
|
|
target_cache = target_cache.next_cache_block
|
|
// Reset `i` after the first iteration.
|
|
i = start
|
|
}
|
|
}
|
|
}
|
|
// Entry must be in the expanded cache blocks.
|
|
assert_contextless(cache.next_cache_block != nil)
|
|
cache = cache.next_cache_block
|
|
}
|
|
}
|
|
|
|
/*
|
|
Make an allocation using contiguous slabs as the backing.
|
|
|
|
This procedure will check through the heap's cache for viable superpages to
|
|
support the allocation.
|
|
*/
|
|
@(require_results)
|
|
heap_cache_get_contiguous_slabs :: proc "contextless" (size: int) -> (ptr: rawptr) {
|
|
contiguous := heap_slabs_needed_for_size(size)
|
|
cache := local_heap_cache
|
|
for {
|
|
for i := 0; i < len(cache.superpages_with_free_slabs); i += 1 {
|
|
if cache.superpages_with_free_slabs[i] == nil {
|
|
// No superpages with free slabs left.
|
|
heap_debug_cover(.Alloc_Slab_Wide_Needed_New_Superpage)
|
|
for {
|
|
superpage := heap_get_superpage()
|
|
heap_link_superpage(superpage)
|
|
heap_cache_register_superpage(superpage)
|
|
if superpage.free_slabs >= contiguous {
|
|
alloc, ok := heap_make_slab_sized_allocation(superpage, size)
|
|
if ok {
|
|
return alloc
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
heap_debug_cover(.Alloc_Slab_Wide_Used_Available_Superpage)
|
|
superpage := cache.superpages_with_free_slabs[i]
|
|
if superpage.free_slabs >= contiguous {
|
|
alloc, ok := heap_make_slab_sized_allocation(superpage, size)
|
|
if ok {
|
|
return alloc
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert_contextless(cache.next_cache_block != nil)
|
|
cache = cache.next_cache_block
|
|
}
|
|
}
|
|
|
|
/*
|
|
Add a superpage with free slabs to the heap's cache.
|
|
*/
|
|
heap_cache_add_superpage_with_free_slabs :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
assert_contextless(intrinsics.atomic_load_explicit(&superpage.owner, .Acquire) == get_current_thread_id(), "The heap allocator tried to cache a superpage that does not belong to it.")
|
|
cache := local_heap_cache
|
|
|
|
for {
|
|
for i := 0; i < len(cache.superpages_with_free_slabs); i += 1 {
|
|
if cache.superpages_with_free_slabs[i] == nil {
|
|
cache.superpages_with_free_slabs[i] = superpage
|
|
return
|
|
}
|
|
}
|
|
assert_contextless(cache.next_cache_block != nil)
|
|
cache = cache.next_cache_block
|
|
}
|
|
}
|
|
|
|
/*
|
|
Remove a superpage from the heap's cache for superpages with free slabs.
|
|
*/
|
|
heap_cache_remove_superpage_with_free_slabs :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
cache := local_heap_cache
|
|
for {
|
|
for i := 0; i < len(cache.superpages_with_free_slabs); i += 1 {
|
|
if cache.superpages_with_free_slabs[i] == superpage {
|
|
// Swap with the tail.
|
|
source_i := i
|
|
target_cache := cache
|
|
i += 1
|
|
for {
|
|
for j := i; j < len(cache.superpages_with_free_slabs); j += 1 {
|
|
if target_cache.superpages_with_free_slabs[j] == nil {
|
|
cache.superpages_with_free_slabs[source_i] = target_cache.superpages_with_free_slabs[j-1]
|
|
target_cache.superpages_with_free_slabs[j-1] = nil
|
|
return
|
|
}
|
|
}
|
|
if target_cache.next_cache_block == nil {
|
|
// The entry is at the end of the list and we have run
|
|
// out of space to search.
|
|
cache.superpages_with_free_slabs[source_i] = nil
|
|
assert_contextless(source_i == len(cache.superpages_with_free_slabs), "The heap allocator tried to remove a non-terminal superpage with free slabs entry when it should have swapped it with the tail.")
|
|
return
|
|
} else if target_cache.next_cache_block.superpages_with_free_slabs[0] == nil {
|
|
// The next list section is empty, so we have to end here.
|
|
cache.superpages_with_free_slabs[source_i] = target_cache.superpages_with_free_slabs[len(cache.superpages_with_free_slabs)-1]
|
|
target_cache.superpages_with_free_slabs[len(cache.superpages_with_free_slabs)-1] = nil
|
|
return
|
|
}
|
|
target_cache = target_cache.next_cache_block
|
|
// Reset `i` after the first iteration.
|
|
i = 0
|
|
}
|
|
}
|
|
}
|
|
assert_contextless(cache.next_cache_block != nil)
|
|
cache = cache.next_cache_block
|
|
}
|
|
}
|
|
|
|
//
|
|
// Superpage Orphanage
|
|
//
|
|
|
|
// This construction is used to avoid the ABA problem.
|
|
//
|
|
// Virtually all systems we support should have a 64-bit CAS to make this work.
|
|
Tagged_Pointer :: bit_field u64 {
|
|
// Intel 5-level paging uses up to 56 bits of a pointer on x86-64.
|
|
// This should be enough to cover ARM64, too.
|
|
//
|
|
// We use an `i64` here to maintain sign extension on the upper bits for
|
|
// systems where this is relevant, hence the extra bit over 56.
|
|
pointer: i64 | 57,
|
|
// We only need so many bits that enough transactions don't happen so fast
|
|
// as to roll over the value to cause a situation where ABA can manifest.
|
|
version: u8 | 7,
|
|
}
|
|
|
|
/*
|
|
Put a Superpage into the global orphanage.
|
|
|
|
The caller is responsible for fetch-adding the count.
|
|
*/
|
|
heap_push_orphan :: proc "contextless" (superpage: ^Heap_Superpage) {
|
|
assert_contextless(intrinsics.atomic_load_explicit(&superpage.owner, .Acquire) != 0, "The heap allocator tried to push an unowned superpage to the orphanage.")
|
|
|
|
// The algorithm below is one of the well-known methods of resolving the
|
|
// ABA problem, known as a tagged pointer. The gist is that if another
|
|
// thread has changed the value, we'll be able to detect that by checking
|
|
// against the version bits.
|
|
old_head := transmute(Tagged_Pointer)intrinsics.atomic_load_explicit(cast(^u64)&heap_orphanage, .Relaxed)
|
|
intrinsics.atomic_store_explicit(&superpage.master_cache_block, nil, .Seq_Cst)
|
|
// NOTE: This next instruction must not float above the previous one, as
|
|
// this superpage could host the `master_cache_block`. The order is important
|
|
// to keep other threads from trying to access it while we're clearing it.
|
|
if superpage.cache_block.in_use {
|
|
intrinsics.mem_zero_volatile(&superpage.cache_block, size_of(Heap_Cache_Block))
|
|
heap_debug_cover(.Superpage_Cache_Block_Cleared)
|
|
}
|
|
superpage.prev = nil
|
|
intrinsics.atomic_store_explicit(&superpage.owner, 0, .Release)
|
|
for {
|
|
// NOTE: `next` is accessed atomically when pushing or popping from the
|
|
// orphanage, because this field must synchronize with other threads at
|
|
// this point.
|
|
//
|
|
// This has to do mainly with swinging the head's linking pointer.
|
|
//
|
|
// Beyond this point, the thread which owns the superpage will be the
|
|
// only one to read `next`, hence why it is not read atomically
|
|
// anywhere else.
|
|
intrinsics.atomic_store_explicit(&superpage.next, cast(^Heap_Superpage)uintptr(old_head.pointer), .Release)
|
|
new_head: Tagged_Pointer = ---
|
|
new_head.pointer = i64(uintptr(superpage))
|
|
new_head.version = old_head.version + 1
|
|
|
|
old_head_, swapped := intrinsics.atomic_compare_exchange_weak_explicit(cast(^u64)&heap_orphanage, transmute(u64)old_head, transmute(u64)new_head, .Acq_Rel, .Relaxed)
|
|
if swapped {
|
|
break
|
|
}
|
|
old_head = transmute(Tagged_Pointer)old_head_
|
|
}
|
|
}
|
|
|
|
/*
|
|
Remove and return the first entry from the Superpage orphanage, which may be nil.
|
|
*/
|
|
@(require_results)
|
|
heap_pop_orphan :: proc "contextless" () -> (superpage: ^Heap_Superpage) {
|
|
old_head := transmute(Tagged_Pointer)intrinsics.atomic_load_explicit(cast(^u64)&heap_orphanage, .Relaxed)
|
|
for {
|
|
superpage = cast(^Heap_Superpage)uintptr(old_head.pointer)
|
|
if superpage == nil {
|
|
return
|
|
}
|
|
new_head: Tagged_Pointer = ---
|
|
new_head.pointer = i64(uintptr(intrinsics.atomic_load_explicit(&superpage.next, .Acquire)))
|
|
new_head.version = old_head.version + 1
|
|
|
|
old_head_, swapped := intrinsics.atomic_compare_exchange_weak_explicit(cast(^u64)&heap_orphanage, transmute(u64)old_head, transmute(u64)new_head, .Acq_Rel, .Relaxed)
|
|
if swapped {
|
|
intrinsics.atomic_store_explicit(&superpage.next, nil, .Release)
|
|
intrinsics.atomic_store_explicit(&superpage.owner, get_current_thread_id(), .Release)
|
|
intrinsics.atomic_sub_explicit(&heap_orphanage_count, 1, .Release)
|
|
break
|
|
}
|
|
old_head = transmute(Tagged_Pointer)old_head_
|
|
}
|
|
return
|
|
}
|
|
|
|
//
|
|
// Globals
|
|
//
|
|
|
|
@(init, private)
|
|
setup_superpage_orphanage :: proc "contextless" () {
|
|
when !VIRTUAL_MEMORY_SUPPORTED {
|
|
return
|
|
}
|
|
|
|
// Upon a thread's clean exit, this procedure will compact its heap and
|
|
// distribute the superpages into the orphanage, if it has space.
|
|
add_thread_local_cleaner(proc "odin" () {
|
|
for superpage := local_heap; superpage != nil; /**/ {
|
|
next_superpage := superpage.next
|
|
// The following logic is a specialized case of the same found in
|
|
// `compact_heap` that ignores the cache since there's no need to
|
|
// update it when the thread's heap is being broken down.
|
|
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 {
|
|
i += 1
|
|
if slab.bin_size == 0 {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if intrinsics.atomic_load_explicit(&slab.remote_free_bins_scheduled, .Acquire) > 0 {
|
|
heap_merge_remote_frees(slab)
|
|
|
|
if slab.free_bins > 0 {
|
|
// Synchronize with any thread that might be trying to
|
|
// free as we merge.
|
|
intrinsics.atomic_store_explicit(&slab.is_full, false, .Release)
|
|
}
|
|
heap_debug_cover(.Orphaned_Superpage_Merged_Remote_Frees)
|
|
}
|
|
|
|
if slab.free_bins == slab.max_bins {
|
|
if slab.bin_size > HEAP_MAX_BIN_SIZE {
|
|
heap_free_wide_slab(superpage, slab)
|
|
} else {
|
|
heap_free_slab(superpage, slab)
|
|
}
|
|
heap_debug_cover(.Orphaned_Superpage_Freed_Slab)
|
|
}
|
|
}
|
|
|
|
if superpage.free_slabs == HEAP_SLAB_COUNT {
|
|
if intrinsics.atomic_add_explicit(&heap_orphanage_count, 1, .Acq_Rel) >= HEAP_MAX_EMPTY_ORPHANED_SUPERPAGES {
|
|
intrinsics.atomic_sub_explicit(&heap_orphanage_count, 1, .Relaxed)
|
|
|
|
free_virtual_memory(superpage, SUPERPAGE_SIZE)
|
|
heap_debug_cover(.Superpage_Freed_By_Exiting_Thread)
|
|
} else {
|
|
heap_push_orphan(superpage)
|
|
heap_debug_cover(.Superpage_Orphaned_By_Exiting_Thread)
|
|
}
|
|
} else {
|
|
intrinsics.atomic_add_explicit(&heap_orphanage_count, 1, .Release)
|
|
heap_push_orphan(superpage)
|
|
heap_debug_cover(.Superpage_Orphaned_By_Exiting_Thread)
|
|
}
|
|
|
|
superpage = next_superpage
|
|
}
|
|
})
|
|
}
|
|
|
|
// This is a lock-free, intrusively singly-linked list of Superpages that are
|
|
// not in any thread's heap. They are free for adoption by other threads
|
|
// needing memory.
|
|
heap_orphanage: Tagged_Pointer
|
|
|
|
// This is an _estimate_ of the number of Superpages in the orphanage. It can
|
|
// never be entirely accurate due to the nature of the design.
|
|
heap_orphanage_count: int
|
|
|
|
@(thread_local) local_heap: ^Heap_Superpage
|
|
@(thread_local) local_heap_tail: ^Heap_Superpage
|
|
@(thread_local) local_heap_cache: ^Heap_Cache_Block
|
|
|
|
//
|
|
// API
|
|
//
|
|
|
|
/*
|
|
Allocate an arbitrary amount of memory from the heap and optionally zero it.
|
|
*/
|
|
@(require_results)
|
|
heap_alloc :: proc "contextless" (size: int, zero_memory: bool = true) -> (ptr: rawptr) {
|
|
assert_contextless(size >= 0, "The heap allocator was given a negative size.")
|
|
|
|
// Handle Huge allocations.
|
|
if size >= HEAP_HUGE_ALLOCATION_THRESHOLD {
|
|
heap_debug_cover(.Alloc_Huge)
|
|
return heap_make_huge_allocation(size)
|
|
}
|
|
|
|
// Initialize the heap if needed.
|
|
if local_heap == nil {
|
|
local_heap = heap_get_superpage()
|
|
local_heap_tail = local_heap
|
|
local_heap_cache = &local_heap.cache_block
|
|
local_heap_cache.in_use = true
|
|
heap_cache_register_superpage(local_heap)
|
|
heap_debug_cover(.Alloc_Heap_Initialized)
|
|
}
|
|
|
|
// Take care of any remote frees.
|
|
remote_free_count := intrinsics.atomic_load_explicit(&local_heap_cache.remote_free_count, .Acquire)
|
|
if remote_free_count > 0 {
|
|
cache := local_heap_cache
|
|
removed := 0
|
|
counter := remote_free_count
|
|
// Go through all superpages in the cache for superpages with remote
|
|
// frees to see if any still have frees needing merged.
|
|
merge_loop: for {
|
|
consume_loop: for i in 0..<len(cache.superpages_with_remote_frees) {
|
|
superpage := intrinsics.atomic_load_explicit(&cache.superpages_with_remote_frees[i], .Acquire)
|
|
if superpage == nil {
|
|
continue consume_loop
|
|
}
|
|
|
|
// First, we will remove the cache entry, to avoid issues with
|
|
// freeing operations that happen simultaneously while we're
|
|
// merging.
|
|
//
|
|
// For example, if we merge the freed bins in one slab while a
|
|
// lower-index slab receives a remote free, we would otherwise
|
|
// be unaware of that, and if the superpage was still marked as
|
|
// being in the cache, the freeing thread would not schedule
|
|
// it.
|
|
//
|
|
// The performance hit should be negligible in the event that
|
|
// we have to reschedule the superpage.
|
|
intrinsics.atomic_store_explicit(&cache.superpages_with_remote_frees[i], nil, .Release)
|
|
// Seq_Cst, to prevent these two from being seen out of order.
|
|
intrinsics.atomic_store_explicit(&superpage.remote_free_set, false, .Seq_Cst)
|
|
should_reschedule := false
|
|
|
|
for j := 0; j < HEAP_SLAB_COUNT; /**/ {
|
|
slab := heap_superpage_index_slab(superpage, j)
|
|
bin_size := slab.bin_size
|
|
// We only bother to merge if the slab is in use, full, and
|
|
// signals that it has remote frees. This is for the sake
|
|
// of speed, given how large the bitmaps can be.
|
|
merge_block: if bin_size > 0 && slab.free_bins == 0 && intrinsics.atomic_load_explicit(&slab.remote_free_bins_scheduled, .Acquire) > 0 {
|
|
heap_merge_remote_frees(slab)
|
|
if slab.free_bins == 0 {
|
|
// No bins were freed at all, which is possible due
|
|
// to the parallel nature of this code. The freeing
|
|
// thread could have signalled its intent, but we
|
|
// merged before it had a chance to flip the
|
|
// necessary bit.
|
|
should_reschedule = true
|
|
break merge_block
|
|
}
|
|
intrinsics.atomic_store_explicit(&slab.is_full, false, .Release)
|
|
|
|
if bin_size > HEAP_MAX_BIN_SIZE {
|
|
heap_free_wide_slab(superpage, slab)
|
|
} else {
|
|
if slab.free_bins == slab.max_bins {
|
|
heap_free_slab(superpage, slab)
|
|
} else {
|
|
heap_cache_add_slab(slab, heap_bin_size_to_rank(bin_size))
|
|
}
|
|
}
|
|
}
|
|
|
|
if bin_size > HEAP_MAX_BIN_SIZE {
|
|
// Skip contiguous slabs.
|
|
j += heap_slabs_needed_for_size(bin_size)
|
|
} else {
|
|
j += 1
|
|
}
|
|
}
|
|
|
|
if should_reschedule {
|
|
// This is the logic found in `heap_remote_cache_add_remote_free_superpage`, simplified.
|
|
if !intrinsics.atomic_exchange_explicit(&superpage.remote_free_set, true, .Acq_Rel) {
|
|
cache_reschedule := local_heap_cache
|
|
reschedule_loop: for {
|
|
for j := 0; j < len(cache_reschedule.superpages_with_remote_frees); j += 1 {
|
|
old, swapped := intrinsics.atomic_compare_exchange_strong_explicit(&cache_reschedule.superpages_with_remote_frees[j], nil, superpage, .Acq_Rel, .Relaxed)
|
|
assert_contextless(old != superpage, "The heap allocator found a duplicate of a superpage in its superpages_with_remote_frees while rescheduling.")
|
|
if swapped {
|
|
intrinsics.atomic_add_explicit(&local_heap_cache.remote_free_count, 1, .Release)
|
|
break reschedule_loop
|
|
}
|
|
}
|
|
next_cache_block := intrinsics.atomic_load_explicit(&cache_reschedule.next_cache_block, .Acquire)
|
|
assert_contextless(next_cache_block != nil, "The heap allocator failed to find free space for a new entry in its superpages_with_remote_frees cache.")
|
|
cache_reschedule = next_cache_block
|
|
}
|
|
}
|
|
} else {
|
|
removed += 1
|
|
}
|
|
|
|
counter -= 1
|
|
if counter == 0 {
|
|
break merge_loop
|
|
}
|
|
}
|
|
if cache.next_cache_block == nil {
|
|
break merge_loop
|
|
}
|
|
assert_contextless(cache.next_cache_block != nil)
|
|
cache = cache.next_cache_block
|
|
}
|
|
intrinsics.atomic_sub_explicit(&local_heap_cache.remote_free_count, removed, .Release)
|
|
heap_debug_cover(.Alloc_Collected_Remote_Frees)
|
|
}
|
|
|
|
// Handle slab-wide allocations.
|
|
if size > HEAP_MAX_BIN_SIZE {
|
|
heap_debug_cover(.Alloc_Slab_Wide)
|
|
return heap_cache_get_contiguous_slabs(size)
|
|
}
|
|
|
|
// Get a suitable slab from the heap.
|
|
rounded_size := heap_round_to_bin_size(size)
|
|
slab := heap_cache_get_slab(rounded_size)
|
|
assert_contextless(slab.bin_size == rounded_size, "The heap allocator found a slab with the wrong bin size during allocation.")
|
|
|
|
// Allocate a bin inside the slab.
|
|
sector := slab.next_free_sector
|
|
sector_bits := slab.local_free[sector]
|
|
assert_contextless(sector_bits != 0, "The heap allocator found a slab with a full next_free_sector.")
|
|
|
|
// Select the lowest free bit.
|
|
index := uintptr(intrinsics.count_trailing_zeros(sector_bits))
|
|
|
|
// Convert the index to a pointer.
|
|
ptr = rawptr(slab.data + (uintptr(sector * SECTOR_BITS) + index) * uintptr(rounded_size))
|
|
when !ODIN_DISABLE_ASSERT {
|
|
base_alignment := min(HEAP_MAX_ALIGNMENT, uintptr(rounded_size))
|
|
assert_contextless(uintptr(ptr) & uintptr(base_alignment-1) == 0, "A pointer allocated by the heap is not well-aligned.")
|
|
}
|
|
|
|
// Clear the free bit.
|
|
slab.local_free[sector] &~= (1 << index)
|
|
|
|
// Zero the memory, if needed.
|
|
if zero_memory && index < uintptr(slab.dirty_bins) {
|
|
// Ensure that the memory zeroing is not optimized out by the compiler.
|
|
intrinsics.mem_zero_volatile(ptr, rounded_size)
|
|
// NOTE: A full memory fence should not be needed for any newly-zeroed
|
|
// allocation, as each thread controls its own heap, and for one thread
|
|
// to pass a memory address to another implies some secondary
|
|
// synchronization method, such as a mutex, which would be the way by
|
|
// which the threads come to agree on the state of main memory.
|
|
heap_debug_cover(.Alloc_Zeroed_Memory)
|
|
}
|
|
|
|
// Update statistics.
|
|
slab.dirty_bins = int(max(slab.dirty_bins, 1 + sector * SECTOR_BITS + int(index)))
|
|
slab.free_bins -= 1
|
|
|
|
// Remove the slab from the cache if the slab's full.
|
|
// Otherwise, update the next free sector if the sector's full.
|
|
if slab.free_bins == 0 {
|
|
slab.next_free_sector = slab.sectors
|
|
if intrinsics.atomic_load_explicit(&slab.remote_free_bins_scheduled, .Seq_Cst) > 0 {
|
|
heap_merge_remote_frees(slab)
|
|
if slab.free_bins == 0 {
|
|
// We have come before the other thread, and the bit we needed to find was not set.
|
|
// Treat it as if it is full anyway.
|
|
intrinsics.atomic_store_explicit(&slab.is_full, true, .Release)
|
|
heap_cache_remove_slab(slab, heap_bin_size_to_rank(rounded_size))
|
|
} else {
|
|
// No cache adjustment happens here; we know it's already in the cache.
|
|
}
|
|
} else {
|
|
intrinsics.atomic_store_explicit(&slab.is_full, true, .Release)
|
|
heap_cache_remove_slab(slab, heap_bin_size_to_rank(rounded_size))
|
|
}
|
|
} else {
|
|
if slab.local_free[sector] == 0 {
|
|
sector += 1
|
|
for /**/; sector < slab.sectors; sector += 1 {
|
|
if slab.local_free[sector] != 0 {
|
|
break
|
|
}
|
|
}
|
|
slab.next_free_sector = sector
|
|
}
|
|
}
|
|
heap_debug_cover(.Alloc_Bin)
|
|
return
|
|
}
|
|
|
|
/*
|
|
Free memory returned by `heap_alloc`.
|
|
*/
|
|
heap_free :: proc "contextless" (ptr: rawptr) {
|
|
// Check for nil.
|
|
if ptr == nil {
|
|
return
|
|
}
|
|
|
|
superpage := find_superpage_from_pointer(ptr)
|
|
|
|
// Check if this is a huge allocation.
|
|
if superpage.huge_size > 0 {
|
|
// NOTE: If the allocator is passed a pointer that it does not own,
|
|
// which is a scenario that it cannot possibly detect (in any
|
|
// reasonably performant fashion), then the condition above may result
|
|
// in a segmentation violation.
|
|
//
|
|
// Regardless, the result of passing a pointer to this heap allocator
|
|
// which it did not return is undefined behavior.
|
|
free_virtual_memory(superpage, superpage.huge_size)
|
|
heap_debug_cover(.Freed_Huge_Allocation)
|
|
return
|
|
}
|
|
|
|
// Find which slab this pointer belongs to.
|
|
slab := find_slab_from_pointer(ptr)
|
|
assert_contextless(slab.bin_size > 0, "The heap allocator tried to free a pointer belonging to an empty slab.")
|
|
|
|
// Check if this is a slab-wide allocation.
|
|
if slab.bin_size > HEAP_MAX_BIN_SIZE {
|
|
if intrinsics.atomic_load_explicit(&superpage.owner, .Acquire) == get_current_thread_id() {
|
|
heap_free_wide_slab(superpage, slab)
|
|
heap_debug_cover(.Freed_Wide_Slab)
|
|
} else {
|
|
// Atomically let the owner know there's a free slab.
|
|
intrinsics.atomic_add_explicit(&slab.remote_free_bins_scheduled, 1, .Release)
|
|
old := intrinsics.atomic_or_explicit(&slab.remote_free[0], 1, .Seq_Cst)
|
|
heap_remote_cache_add_remote_free_superpage(superpage)
|
|
|
|
when HEAP_PANIC_ON_DOUBLE_FREE {
|
|
if old == 1 {
|
|
panic_contextless("The heap allocator freed an already-free pointer.")
|
|
}
|
|
}
|
|
heap_debug_cover(.Remotely_Freed_Wide_Slab)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Find which sector and bin this pointer refers to.
|
|
bin_number := int(uintptr(ptr) - slab.data) / slab.bin_size
|
|
sector := bin_number / INTEGER_BITS
|
|
index := uint(bin_number) % INTEGER_BITS
|
|
|
|
assert_contextless(bin_number < slab.max_bins, "Calculated an incorrect bin number for slab.")
|
|
assert_contextless(sector < slab.sectors, "Calculated an incorrect sector for slab.")
|
|
|
|
// See if we own the slab or not, then free the pointer.
|
|
if intrinsics.atomic_load_explicit(&superpage.owner, .Acquire) == get_current_thread_id() {
|
|
when HEAP_PANIC_ON_DOUBLE_FREE {
|
|
if slab.local_free[sector] & (1 << index) != 0 {
|
|
panic_contextless("The heap allocator freed an already-free pointer.")
|
|
}
|
|
}
|
|
// Mark the bin as free.
|
|
slab.local_free[sector] |= 1 << index
|
|
if slab.free_bins == 0 {
|
|
intrinsics.atomic_store_explicit(&slab.is_full, false, .Release)
|
|
// Put this slab back in the available list.
|
|
heap_cache_add_slab(slab, heap_bin_size_to_rank(slab.bin_size))
|
|
heap_debug_cover(.Freed_Bin_Reopened_Full_Slab)
|
|
}
|
|
slab.free_bins += 1
|
|
assert_contextless(slab.free_bins <= slab.max_bins, "A slab of the heap allocator overflowed its free bins.")
|
|
// Free the slab if it's empty and was at one point in time completely
|
|
// full. This is a heuristic to prevent a single repeated new/free
|
|
// operation in an otherwise empty slab from bogging down the
|
|
// allocator.
|
|
if slab.free_bins == slab.max_bins && slab.dirty_bins == slab.max_bins {
|
|
heap_cache_remove_slab(slab, heap_bin_size_to_rank(slab.bin_size))
|
|
heap_free_slab(superpage, slab)
|
|
heap_debug_cover(.Freed_Bin_Freed_Slab_Which_Was_Fully_Used)
|
|
} else {
|
|
slab.next_free_sector = min(slab.next_free_sector, sector)
|
|
heap_debug_cover(.Freed_Bin_Updated_Slab_Next_Free_Sector)
|
|
}
|
|
// Free the entire superpage if it's empty.
|
|
if superpage.free_slabs == HEAP_SLAB_COUNT && !superpage.cache_block.in_use {
|
|
heap_free_superpage(superpage)
|
|
heap_debug_cover(.Freed_Bin_Freed_Superpage)
|
|
}
|
|
} else {
|
|
// Atomically let the owner know there's a free bin.
|
|
|
|
// NOTE: The order of operations here is important.
|
|
//
|
|
// 1. We must first check if the slab is full. If we wait until later,
|
|
// we risk causing a race.
|
|
is_full := intrinsics.atomic_load_explicit(&slab.is_full, .Acquire)
|
|
|
|
// 2. We have to let the owner know that we intend to schedule a remote
|
|
// free. This way, it can keep the cached entry around if it isn't
|
|
// able to merge all of them due to timing differences on our part.
|
|
intrinsics.atomic_add_explicit(&slab.remote_free_bins_scheduled, 1, .Release)
|
|
|
|
// (Technically, the compiler or processor is allowed to re-order the
|
|
// above two operations, and this is okay. However, the next one acts
|
|
// as a full barrier due to its Sequential Consistency ordering.)
|
|
|
|
// 3. Finally, we flip the bit of the bin we want freed. This must be
|
|
// the final operation across all cores, because the owner could
|
|
// already be in the process of merging remote frees, and if our bin
|
|
// was the last one, it has the authorization to wipe the slab and
|
|
// possibly reallocate in the same block of memory.
|
|
//
|
|
// By deferring this to the end, we avoid any possibility of a race,
|
|
// even if multiple threads are in this section.
|
|
old := intrinsics.atomic_or_explicit(&slab.remote_free[sector], 1 << index, .Seq_Cst)
|
|
|
|
// 4. Now we can safely check if the slab is full and add it to the
|
|
// owner's cache if needed.
|
|
if is_full {
|
|
heap_remote_cache_add_remote_free_superpage(superpage)
|
|
heap_debug_cover(.Remotely_Freed_Bin_Caused_Remote_Superpage_Caching)
|
|
}
|
|
|
|
when HEAP_PANIC_ON_DOUBLE_FREE {
|
|
if old & (1 << index) != 0 {
|
|
panic_contextless("The heap allocator freed an already-free pointer.")
|
|
}
|
|
}
|
|
heap_debug_cover(.Remotely_Freed_Bin)
|
|
|
|
// NOTE: It is possible for two threads to be here at the same time,
|
|
// thus violating any sense of order among the actual number of bins
|
|
// free and the reported number.
|
|
//
|
|
// T1 & T2 could flip bits,
|
|
// T2 could increment the free count, then
|
|
// T3 could merge the free bins.
|
|
//
|
|
// This scenario would result in an inconsistent count no matter which
|
|
// way the above procedure is carried out, hence its unreliability.
|
|
}
|
|
}
|
|
|
|
/*
|
|
Resize memory returned by `heap_alloc`.
|
|
*/
|
|
@(require_results)
|
|
heap_resize :: proc "contextless" (old_ptr: rawptr, old_size: int, new_size: int, zero_memory: bool = true) -> (new_ptr: rawptr) {
|
|
Size_Category :: enum {
|
|
Unknown,
|
|
Bin,
|
|
Slab,
|
|
Huge,
|
|
}
|
|
|
|
// We need to first determine if we're crossing size categories.
|
|
old_category: Size_Category
|
|
switch {
|
|
case old_size <= HEAP_MAX_BIN_SIZE: old_category = .Bin
|
|
case old_size < HEAP_HUGE_ALLOCATION_THRESHOLD: old_category = .Slab
|
|
case old_size >= HEAP_HUGE_ALLOCATION_THRESHOLD: old_category = .Huge
|
|
case: unreachable()
|
|
}
|
|
new_category: Size_Category
|
|
switch {
|
|
case new_size <= HEAP_MAX_BIN_SIZE: new_category = .Bin
|
|
case new_size < HEAP_HUGE_ALLOCATION_THRESHOLD: new_category = .Slab
|
|
case new_size >= HEAP_HUGE_ALLOCATION_THRESHOLD: new_category = .Huge
|
|
case: unreachable()
|
|
}
|
|
assert_contextless(old_category != .Unknown)
|
|
assert_contextless(new_category != .Unknown)
|
|
|
|
if new_category != old_category {
|
|
// A change in size category cannot be optimized.
|
|
new_ptr = heap_alloc(new_size, zero_memory)
|
|
intrinsics.mem_copy_non_overlapping(new_ptr, old_ptr, min(old_size, new_size))
|
|
heap_free(old_ptr)
|
|
heap_debug_cover(.Resize_Crossed_Size_Categories)
|
|
return
|
|
}
|
|
|
|
// NOTE: Superpage owners are the only ones that can change the amount of
|
|
// memory that backs each pointer. Other threads have to allocate and copy.
|
|
|
|
// Check if this is a huge allocation.
|
|
superpage := find_superpage_from_pointer(old_ptr)
|
|
if superpage.huge_size > 0 {
|
|
// This block follows the preamble in `heap_make_huge_allocation`.
|
|
new_real_size := new_size
|
|
if new_size < SUPERPAGE_SIZE - HEAP_HUGE_ALLOCATION_BOOK_KEEPING {
|
|
new_real_size = SUPERPAGE_SIZE
|
|
heap_debug_cover(.Resize_Huge_Size_Set_To_Superpage)
|
|
} else {
|
|
new_real_size += HEAP_HUGE_ALLOCATION_BOOK_KEEPING
|
|
heap_debug_cover(.Resize_Huge_Size_Adjusted)
|
|
}
|
|
resized_superpage := cast(^Heap_Superpage)resize_virtual_memory(superpage, superpage.huge_size, new_real_size, SUPERPAGE_SIZE)
|
|
assert_contextless(uintptr(resized_superpage) & (SUPERPAGE_SIZE-1) == 0, "After resizing a huge allocation, the pointer was no longer aligned to a superpage boundary.")
|
|
resized_superpage.huge_size = new_real_size
|
|
u := uintptr(resized_superpage) + HEAP_HUGE_ALLOCATION_BOOK_KEEPING
|
|
new_ptr = rawptr(u - u & (HEAP_MAX_ALIGNMENT-1))
|
|
|
|
if zero_memory && new_size > old_size {
|
|
intrinsics.mem_zero_volatile(
|
|
rawptr(uintptr(new_ptr) + uintptr(old_size)),
|
|
new_size - old_size,
|
|
)
|
|
heap_debug_cover(.Resize_Huge_Caused_Memory_Zeroing)
|
|
}
|
|
|
|
heap_debug_cover(.Resize_Huge)
|
|
return
|
|
}
|
|
|
|
// Find which slab this pointer belongs to.
|
|
slab := find_slab_from_pointer(old_ptr)
|
|
|
|
// Check if this is a slab-wide allocation.
|
|
if slab.bin_size > HEAP_MAX_BIN_SIZE {
|
|
contiguous_old := heap_slabs_needed_for_size(slab.bin_size)
|
|
contiguous_new := heap_slabs_needed_for_size(new_size)
|
|
if contiguous_new == contiguous_old {
|
|
// We already have enough slabs to serve the request.
|
|
if zero_memory && new_size > old_size {
|
|
intrinsics.mem_zero_volatile(
|
|
rawptr(uintptr(old_ptr) + uintptr(old_size)),
|
|
new_size - old_size,
|
|
)
|
|
heap_debug_cover(.Resize_Wide_Slab_Caused_Memory_Zeroing)
|
|
}
|
|
heap_debug_cover(.Resize_Wide_Slab_Kept_Old_Pointer)
|
|
return old_ptr
|
|
}
|
|
|
|
if slab.index + contiguous_new >= HEAP_SLAB_COUNT {
|
|
// Expanding this slab would go beyond the Superpage.
|
|
// We need more memory.
|
|
new_ptr = heap_alloc(new_size, zero_memory)
|
|
intrinsics.mem_copy_non_overlapping(new_ptr, old_ptr, min(old_size, new_size))
|
|
heap_free(old_ptr)
|
|
return
|
|
}
|
|
|
|
if intrinsics.atomic_load_explicit(&superpage.owner, .Acquire) != get_current_thread_id() {
|
|
// We are not the owner of this data, therefore none of the special
|
|
// optimized paths for wide slabs are available to us, as they all
|
|
// involve touching the Superpage.
|
|
//
|
|
// We must re-allocate.
|
|
new_ptr = heap_alloc(new_size, zero_memory)
|
|
intrinsics.mem_copy_non_overlapping(new_ptr, old_ptr, min(old_size, new_size))
|
|
heap_free(old_ptr)
|
|
heap_debug_cover(.Resize_Wide_Slab_From_Remote_Thread)
|
|
return
|
|
}
|
|
|
|
// Can we shrink the wide slab, or can we expand it in-place?
|
|
if contiguous_new < contiguous_old {
|
|
previously_full_superpage := superpage.free_slabs == 0
|
|
for i := slab.index + contiguous_new; i < slab.index + contiguous_old; i += 1 {
|
|
// Mark the latter slabs as unused.
|
|
next_slab := heap_superpage_index_slab(superpage, i)
|
|
next_slab.index = i
|
|
next_slab.is_dirty = true
|
|
next_slab.bin_size = 0
|
|
}
|
|
superpage.next_free_slab_index = min(superpage.next_free_slab_index, slab.index + contiguous_new)
|
|
superpage.free_slabs += contiguous_old - contiguous_new
|
|
slab.bin_size = new_size
|
|
if previously_full_superpage {
|
|
heap_cache_add_superpage_with_free_slabs(superpage)
|
|
heap_debug_cover(.Superpage_Added_To_Open_Cache_By_Resizing_Wide_Slab)
|
|
}
|
|
heap_debug_cover(.Resize_Wide_Slab_Shrunk_In_Place)
|
|
} else {
|
|
// NOTE: We've already guarded against going beyond `HEAP_SLAB_COUNT` in the section above.
|
|
for i := slab.index + contiguous_old; i < slab.index + contiguous_new; i += 1 {
|
|
if heap_superpage_index_slab(superpage, i).bin_size != 0 {
|
|
// Contiguous space is unavailable.
|
|
new_ptr = heap_alloc(new_size, zero_memory)
|
|
intrinsics.mem_copy_non_overlapping(new_ptr, old_ptr, min(old_size, new_size))
|
|
heap_free(old_ptr)
|
|
heap_debug_cover(.Resize_Wide_Slab_Failed_To_Find_Contiguous_Expansion)
|
|
return
|
|
}
|
|
}
|
|
for i := slab.index + contiguous_old; i < slab.index + contiguous_new; i += 1 {
|
|
// Wipe the index bits, and if needed, the rest of the data.
|
|
next_slab := heap_superpage_index_slab(superpage, i)
|
|
next_slab.index = 0
|
|
if next_slab.is_dirty {
|
|
heap_slab_clear_data(next_slab)
|
|
}
|
|
}
|
|
// We must update the slab's `bin_size` before calling
|
|
// `heap_update_next_free_slab`, as that procedure will need to
|
|
// iterate over the slabs.
|
|
slab.bin_size = new_size
|
|
superpage.free_slabs += contiguous_old - contiguous_new
|
|
if superpage.free_slabs == 0 {
|
|
heap_cache_remove_superpage_with_free_slabs(superpage)
|
|
} else {
|
|
heap_update_next_free_slab_index(superpage, 0)
|
|
}
|
|
heap_debug_cover(.Resize_Wide_Slab_Expanded_In_Place)
|
|
}
|
|
|
|
// The slab-wide allocation has been resized in-place.
|
|
|
|
return old_ptr
|
|
}
|
|
|
|
// See if a bin rank change is needed.
|
|
new_rounded_size := heap_round_to_bin_size(new_size)
|
|
if slab.bin_size == new_rounded_size {
|
|
if zero_memory && new_size > old_size {
|
|
intrinsics.mem_zero_volatile(
|
|
rawptr(uintptr(old_ptr) + uintptr(old_size)),
|
|
new_size - old_size,
|
|
)
|
|
// It could be argued that a full memory fence is necessary here,
|
|
// because one thread may resize an address known to other threads,
|
|
// but as is the case with zeroing during allocation, we treat this
|
|
// as if the state change is not independent of the allocator.
|
|
//
|
|
// That is to say, if one thread resizes an address in-place, it's
|
|
// expected that other threads will need to be notified of this by
|
|
// the program, as with any other synchronization.
|
|
heap_debug_cover(.Resize_Caused_Memory_Zeroing)
|
|
}
|
|
heap_debug_cover(.Resize_Kept_Old_Pointer)
|
|
return old_ptr
|
|
}
|
|
|
|
// Allocate and copy, as a last resort.
|
|
new_ptr = heap_alloc(new_size, zero_memory)
|
|
intrinsics.mem_copy_non_overlapping(new_ptr, old_ptr, min(old_size, new_size))
|
|
heap_free(old_ptr)
|
|
return
|
|
}
|