Progress on lifting the 'grime' module to its own package

This commit is contained in:
2024-05-31 19:31:27 -04:00
parent d63242ac9c
commit e84ec719b3
9 changed files with 68 additions and 38 deletions

View File

@ -37,13 +37,8 @@ array_underlying_slice :: proc(slice: []($ Type)) -> Array(Type)
return array_ptr ^
}
array_to_slice :: proc( using self : Array($ Type) ) -> []Type {
return slice_ptr( data, int(num) )
}
array_to_slice_capacity :: proc( using self : Array($ Type) ) -> []Type {
return slice_ptr( data, int(capacity))
}
array_to_slice :: #force_inline proc( using self : Array($ Type) ) -> []Type { return slice_ptr( data, int(num)) }
array_to_slice_capacity :: #force_inline proc( using self : Array($ Type) ) -> []Type { return slice_ptr( data, int(capacity)) }
array_grow_formula :: proc( value : u64 ) -> u64 {
result := (2 * value) + 8
@ -71,22 +66,6 @@ array_init :: proc( $Array_Type : typeid/Array($Type), capacity : u64,
return
}
array_append_value :: proc( self : ^Array( $ Type), value : Type ) -> AllocatorError
{
// profile(#procedure)
if self.header.num == self.header.capacity
{
grow_result := array_grow( self, self.header.capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
self.header.data[ self.header.num ] = value
self.header.num += 1
return AllocatorError.None
}
array_append_array :: proc( using self: ^Array( $ Type), other : Array(Type)) -> AllocatorError
{
if num + other.num > capacity
@ -127,7 +106,23 @@ array_append_slice :: proc( using self : ^Array( $ Type ), items : []Type ) -> A
return AllocatorError.None
}
array_append_at :: proc( using self : ^Array( $ Type ), item : Type, id : u64 ) -> AllocatorError
array_append_value :: proc( self : ^Array( $ Type), value : Type ) -> AllocatorError
{
// profile(#procedure)
if self.header.num == self.header.capacity
{
grow_result := array_grow( self, self.header.capacity )
if grow_result != AllocatorError.None {
return grow_result
}
}
self.header.data[ self.header.num ] = value
self.header.num += 1
return AllocatorError.None
}
array_append_at_value :: proc( using self : ^Array( $ Type ), item : Type, id : u64 ) -> AllocatorError
{
id := id
if id >= num {

View File

@ -0,0 +1,225 @@
/*
Separate chaining hashtable with tombstone (vacancy aware)
This is an alternative to odin's map and the zpl hashtable I first used for this codebase.
So this is a hahstable loosely based at what I saw in the raddbg codebase.
It uses a fixed-size lookup table for the base layer of entries that can be chained.
Each slot keeps track of its vacancy (tombstone, is occupied).
If its occupied a new slot is chained using the fixed bucket-size pool allocator which will have its blocks sized to the type of the table.
This is ideal for tables have an indeterminate scope for how entires are added,
and direct pointers are kept across the codebase instead of a key to the slot.
*/
package grime
import "core:mem"
HTable_Minimum_Capacity :: 4 * Kilobyte
HMapChainedSlot :: struct( $Type : typeid ) {
using links : DLL_NodePN(HMapChainedSlot(Type)),
value : Type,
key : u64,
occupied : b32,
}
HMapChainedHeader :: struct( $ Type : typeid ) {
pool : Pool,
lookup : [] ^HMapChainedSlot(Type),
}
HMapChained :: struct( $ Type : typeid) {
using header : ^HMapChainedHeader(Type),
}
// Provides the nearest prime number value for the given capacity
hmap_closest_prime :: proc( capacity : uint ) -> uint
{
prime_table : []uint = {
53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
6291469, 12582917, 25165843, 50331653, 100663319,
201326611, 402653189, 805306457, 1610612741, 3221225473, 6442450941
};
for slot in prime_table {
if slot >= capacity {
return slot
}
}
return prime_table[len(prime_table) - 1]
}
hmap_chained_init :: proc( $HMapChainedType : typeid/HMapChained($Type), lookup_capacity : uint,
allocator := context.allocator,
pool_bucket_cap : uint = 1 * Kilo,
pool_bucket_reserve_num : uint = 0,
pool_alignment : uint = mem.DEFAULT_ALIGNMENT,
// dbg_name : string = ""
) -> (table : HMapChained(Type), error : AllocatorError)
{
header_size := size_of(HMapChainedHeader(Type))
size := header_size + int(lookup_capacity) * size_of( ^HMapChainedSlot(Type)) + size_of(int)
raw_mem : rawptr
raw_mem, error = alloc( size, allocator = allocator )
if error != AllocatorError.None do return
table.header = cast( ^HMapChainedHeader(Type)) raw_mem
table.pool, error = pool_init(
should_zero_buckets = false,
block_size = size_of(HMapChainedSlot(Type)),
bucket_capacity = pool_bucket_cap,
bucket_reserve_num = pool_bucket_reserve_num,
alignment = pool_alignment,
allocator = allocator,
// dbg_name = str_intern(str_fmt("%v: pool", dbg_name)).str
)
data := transmute([^] ^HMapChainedSlot(Type)) (transmute( [^]HMapChainedHeader(Type)) table.header)[1:]
table.lookup = slice_ptr( data, int(lookup_capacity) )
return
}
hmap_chained_clear :: proc( using self : HMapChained($Type))
{
for slot in lookup
{
if slot == nil {
continue
}
for probe_slot = slot.next; probe_slot != nil; probe_slot = probe_slot.next {
slot.occupied = false
}
slot.occupied = false
}
}
hmap_chained_destroy :: proc( using self : ^HMapChained($Type)) {
pool_destroy( pool )
free( self.header, backing)
self = nil
}
hmap_chained_lookup_id :: #force_inline proc( using self : HMapChained($Type), key : u64 ) -> u64
{
hash_index := key % u64( len(lookup) )
return hash_index
}
hmap_chained_get :: proc( using self : HMapChained($Type), key : u64) -> ^Type
{
// profile(#procedure)
surface_slot := lookup[hmap_chained_lookup_id(self, key)]
if surface_slot == nil {
return nil
}
if surface_slot.occupied && surface_slot.key == key {
return & surface_slot.value
}
for slot := surface_slot.next; slot != nil; slot = slot.next {
if slot.occupied && slot.key == key {
return & surface_slot.value
}
}
return nil
}
hmap_chained_reload :: proc( self : HMapChained($Type), allocator : Allocator )
{
pool_reload(self.pool, allocator)
}
// Returns true if an slot was actually found and marked as vacant
// Entries already found to be vacant will not return true
hmap_chained_remove :: proc( self : HMapChained($Type), key : u64 ) -> b32
{
surface_slot := lookup[hmap_chained_lookup_id(self, key)]
if surface_slot == nil {
return false
}
if surface_slot.occupied && surface_slot.key == key {
surface_slot.occupied = false
return true
}
for slot := surface_slot.next; slot != nil; slot.next
{
if slot.occupied && slot.key == key {
slot.occupied = false
return true
}
}
return false
}
// Sets the value to a vacant slot
// Will preemptively allocate the next slot in the hashtable if its null for the slot.
hmap_chained_set :: proc( using self : HMapChained($Type), key : u64, value : Type ) -> (^ Type, AllocatorError)
{
// profile(#procedure)
hash_index := hmap_chained_lookup_id(self, key)
surface_slot := lookup[hash_index]
set_slot :: #force_inline proc( using self : HMapChained(Type),
slot : ^HMapChainedSlot(Type),
key : u64,
value : Type
) -> (^ Type, AllocatorError )
{
error := AllocatorError.None
if slot.next == nil {
block : []byte
block, error = pool_grab(pool)
next := transmute( ^HMapChainedSlot(Type)) & block[0]
slot.next = next
next.prev = slot
}
slot.key = key
slot.value = value
slot.occupied = true
return & slot.value, error
}
if surface_slot == nil {
block, error := pool_grab(pool)
surface_slot := transmute( ^HMapChainedSlot(Type)) & block[0]
surface_slot.key = key
surface_slot.value = value
surface_slot.occupied = true
if error != AllocatorError.None {
ensure(error != AllocatorError.None, "Allocation failure for chained slot in hash table")
return nil, error
}
lookup[hash_index] = surface_slot
block, error = pool_grab(pool)
next := transmute( ^HMapChainedSlot(Type)) & block[0]
surface_slot.next = next
next.prev = surface_slot
return & surface_slot.value, error
}
if ! surface_slot.occupied
{
result, error := set_slot( self, surface_slot, key, value)
return result, error
}
slot := surface_slot.next
for ; slot != nil; slot = slot.next
{
if !slot.occupied
{
result, error := set_slot( self, surface_slot, key, value)
return result, error
}
}
ensure(false, "Somehow got to a null slot that wasn't preemptively allocated from a previus set")
return nil, AllocatorError.None
}

View File

@ -133,6 +133,11 @@ array_append :: proc {
array_append_slice,
}
array_append_at :: proc {
array_append_at_slice,
array_append_at_value,
}
is_power_of_two :: proc {
is_power_of_two_u32,
is_power_of_two_uintptr,
@ -152,6 +157,10 @@ make :: proc {
make_multi_pointer,
}
push :: proc {
stack_push,
}
to_string :: proc {
runes_to_string,
str_builder_to_string,

View File

@ -1,5 +1,12 @@
package grime
Kilo :: Kilobyte
Mega :: Megabyte
Giga :: Gigabyte
Tera :: Terabyte
Peta :: Petabyte
Exa :: Exabyte
is_power_of_two_u32 :: #force_inline proc "contextless" ( value : u32 ) -> b32
{
return value != 0 && ( value & ( value - 1 )) == 0

View File

@ -0,0 +1,361 @@
/*
This is a pool allocator setup to grow incrementally via buckets.
Buckets are stored in singly-linked lists so that allocations aren't necessrily contiguous.
The pool is setup with the intention to only grab single entires from the bucket,
not for a contiguous array of them.
Thus the free-list only tracks the last free entries thrown out by the user,
irrespective of the bucket the originated from.
This means if there is a heavy recyling of entires in a pool
there can be a large discrepancy of memory localicty if buckets are small.
The pool doesn't allocate any buckets on initialization unless the user specifies.
*/
package grime
import "base:intrinsics"
import "base:runtime"
import "core:mem"
import "core:slice"
Pool :: struct {
using header : ^PoolHeader,
}
PoolHeader :: struct {
backing : Allocator,
dbg_name : string,
tracker : MemoryTracker,
zero_bucket : b32,
block_size : uint,
bucket_capacity : uint,
alignment : uint,
free_list_head : ^Pool_FreeBlock,
current_bucket : ^PoolBucket,
bucket_list : DLL_NodeFL( PoolBucket),
}
PoolBucket :: struct {
using nodes : DLL_NodePN( PoolBucket),
next_block : uint,
blocks : [^]byte,
}
Pool_FreeBlock :: struct {
next : ^Pool_FreeBlock,
}
Pool_Check_Release_Object_Validity :: true
pool_init :: proc (
should_zero_buckets : b32,
block_size : uint,
bucket_capacity : uint,
bucket_reserve_num : uint = 0,
alignment : uint = mem.DEFAULT_ALIGNMENT,
allocator : Allocator = context.allocator,
dbg_name : string = "",
) -> ( pool : Pool, alloc_error : AllocatorError )
{
header_size := align_forward_int( size_of(PoolHeader), int(alignment) )
raw_mem : rawptr
raw_mem, alloc_error = alloc( header_size, int(alignment), allocator )
if alloc_error != .None do return
ensure(block_size > 0, "Bad block size provided")
ensure(bucket_capacity > 0, "Bad bucket capacity provided")
pool.header = cast( ^PoolHeader) raw_mem
pool.zero_bucket = should_zero_buckets
pool.backing = allocator
pool.dbg_name = dbg_name
pool.block_size = align_forward_uint(block_size, alignment)
pool.bucket_capacity = bucket_capacity
pool.alignment = alignment
when ODIN_DEBUG {
memtracker_init( & pool.tracker, allocator, Kilobyte * 96, dbg_name )
}
if bucket_reserve_num > 0 {
alloc_error = pool_allocate_buckets( pool, bucket_reserve_num )
}
pool.current_bucket = pool.bucket_list.first
return
}
pool_reload :: proc( pool : Pool, allocator : Allocator ) {
pool.backing = allocator
}
pool_destroy :: proc ( using self : Pool )
{
if bucket_list.first != nil
{
bucket := bucket_list.first
for ; bucket != nil; bucket = bucket.next {
free( bucket, backing )
}
}
free( self.header, backing )
when ODIN_DEBUG {
memtracker_clear( self.tracker )
}
}
pool_allocate_buckets :: proc( pool : Pool, num_buckets : uint ) -> AllocatorError
{
profile(#procedure)
if num_buckets == 0 {
return .Invalid_Argument
}
header_size := cast(uint) align_forward_int( size_of(PoolBucket), int(pool.alignment))
bucket_size := header_size + pool.bucket_capacity
to_allocate := cast(int) (bucket_size * num_buckets)
// log(str_fmt_tmp("Allocating %d bytes for %d buckets with header_size %d bytes & bucket_size %d", to_allocate, num_buckets, header_size, bucket_size ))
bucket_memory : []byte
alloc_error : AllocatorError
pool_validate( pool )
if pool.zero_bucket {
bucket_memory, alloc_error = alloc_bytes( to_allocate, int(pool.alignment), pool.backing )
}
else {
bucket_memory, alloc_error = alloc_bytes_non_zeroed( to_allocate, int(pool.alignment), pool.backing )
}
pool_validate( pool )
// log(str_fmt_tmp("Bucket memory size: %d bytes, without header: %d", len(bucket_memory), len(bucket_memory) - int(header_size)))
if alloc_error != .None {
return alloc_error
}
verify( bucket_memory != nil, "Bucket memory is null")
next_bucket_ptr := cast( [^]byte) raw_data(bucket_memory)
for index in 0 ..< num_buckets
{
bucket := cast( ^PoolBucket) next_bucket_ptr
bucket.blocks = memory_after_header(bucket)
bucket.next_block = 0
// log( str_fmt_tmp("\tPool (%d) allocated bucket: %p start %p capacity: %d (raw: %d)",
// pool.block_size,
// raw_data(bucket_memory),
// bucket.blocks,
// pool.bucket_capacity / pool.block_size,
// pool.bucket_capacity ))
if pool.bucket_list.first == nil {
pool.bucket_list.first = bucket
pool.bucket_list.last = bucket
}
else {
dll_push_back( & pool.bucket_list.last, bucket )
}
// log( str_fmt_tmp("Bucket List First: %p", self.bucket_list.first))
next_bucket_ptr = next_bucket_ptr[ bucket_size: ]
}
return alloc_error
}
pool_grab :: proc( pool : Pool, zero_memory := false ) -> ( block : []byte, alloc_error : AllocatorError )
{
pool := pool
if pool.current_bucket != nil {
if ( pool.current_bucket.blocks == nil ) {
ensure( false, str_fmt("(corruption) current_bucket was wiped %p", pool.current_bucket) )
}
// verify( pool.current_bucket.blocks != nil, str_fmt_tmp("(corruption) current_bucket was wiped %p", pool.current_bucket) )
}
// profile(#procedure)
alloc_error = .None
// Check the free-list first for a block
if pool.free_list_head != nil
{
head := & pool.free_list_head
// Compiler Bug? Fails to compile
// last_free := ll_pop( & pool.free_list_head )
last_free : ^Pool_FreeBlock = pool.free_list_head
pool.free_list_head = pool.free_list_head.next
block = byte_slice( cast([^]byte) last_free, int(pool.block_size) )
// log( str_fmt_tmp("\tReturning free block: %p %d", raw_data(block), pool.block_size))
if zero_memory {
slice.zero(block)
}
when ODIN_DEBUG {
memtracker_register_auto_name_slice( & pool.tracker, block)
}
return
}
if pool.current_bucket == nil
{
alloc_error = pool_allocate_buckets( pool, 1 )
if alloc_error != .None {
ensure(false, "Failed to allocate bucket")
return
}
pool.current_bucket = pool.bucket_list.first
// log( "First bucket allocation")
}
next := uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block)
end := uintptr(pool.current_bucket.blocks) + uintptr(pool.bucket_capacity)
blocks_left, overflow_signal := intrinsics.overflow_sub( end, next )
if blocks_left == 0 || overflow_signal
{
// Compiler Bug
// if current_bucket.next != nil {
if pool.current_bucket.next != nil {
// current_bucket = current_bucket.next
// log( str_fmt_tmp("\tBucket %p exhausted using %p", pool.current_bucket, pool.current_bucket.next))
pool.current_bucket = pool.current_bucket.next
verify( pool.current_bucket.blocks != nil, "New current_bucket's blocks are null (new current_bucket is corrupted)" )
}
else
{
// log( "\tAll previous buckets exhausted, allocating new bucket")
alloc_error := pool_allocate_buckets( pool, 1 )
if alloc_error != .None {
ensure(false, "Failed to allocate bucket")
return
}
pool.current_bucket = pool.current_bucket.next
verify( pool.current_bucket.blocks != nil, "Next's blocks are null (Post new bucket alloc)" )
}
}
verify( pool.current_bucket != nil, "Attempted to grab a block from a null bucket reference" )
// Compiler Bug
// block = slice_ptr( current_bucket.blocks[ current_bucket.next_block:], int(block_size) )
// self.current_bucket.next_block += block_size
block_ptr := cast(rawptr) (uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block))
block = byte_slice( block_ptr, int(pool.block_size) )
pool.current_bucket.next_block += pool.block_size
next = uintptr(pool.current_bucket.blocks) + uintptr(pool.current_bucket.next_block)
// log( str_fmt_tmp("\tgrabbing block: %p from %p blocks left: %d", raw_data(block), pool.current_bucket.blocks, (end - next) / uintptr(pool.block_size) ))
if zero_memory {
slice.zero(block)
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", block_ptr, cast(rawptr) (uintptr(block_ptr) + uintptr(pool.block_size))))
}
when ODIN_DEBUG {
memtracker_register_auto_name_slice( & pool.tracker, block)
}
return
}
pool_release :: proc( self : Pool, block : []byte, loc := #caller_location )
{
// profile(#procedure)
if Pool_Check_Release_Object_Validity {
within_bucket := pool_validate_ownership( self, block )
verify( within_bucket, "Attempted to release data that is not within a bucket of this pool", location = loc )
}
// Compiler bug
// ll_push( & self.free_list_head, cast(^Pool_FreeBlock) raw_data(block) )
pool_watch := self
head_watch := & self.free_list_head
// ll_push:
new_free_block := cast(^Pool_FreeBlock) raw_data(block)
(new_free_block ^) = {}
new_free_block.next = self.free_list_head
self.free_list_head = new_free_block
// new_free_block = new_free_block
// log( str_fmt_tmp("Released block: %p %d", new_free_block, self.block_size))
start := new_free_block
end := transmute(rawptr) (uintptr(new_free_block) + uintptr(self.block_size) - 1)
when ODIN_DEBUG {
memtracker_unregister( self.tracker, { start, end } )
}
}
pool_reset :: proc( using pool : Pool )
{
bucket : ^PoolBucket = bucket_list.first // TODO(Ed): Compiler bug? Build fails unless ^PoolBucket is explcitly specified.
for ; bucket != nil; {
bucket.next_block = 0
}
pool.free_list_head = nil
pool.current_bucket = bucket_list.first
}
pool_validate :: proc( pool : Pool )
{
when !ODIN_DEBUG do return
pool := pool
// Make sure all buckets don't show any indication of corruption
bucket : ^PoolBucket = pool.bucket_list.first
if bucket != nil && uintptr(bucket) < 0x10000000000 {
ensure(false, str_fmt("Found a corrupted bucket %p", bucket ))
}
// Compiler bug ^^ same as pool_reset
for ; bucket != nil; bucket = bucket.next
{
if bucket != nil && uintptr(bucket) < 0x10000000000 {
ensure(false, str_fmt("Found a corrupted bucket %p", bucket ))
}
if ( bucket.blocks == nil ) {
ensure(false, str_fmt("Found a corrupted bucket %p", bucket ))
}
}
}
pool_validate_ownership :: proc( using self : Pool, block : [] byte ) -> b32
{
profile(#procedure)
within_bucket := b32(false)
// Compiler Bug : Same as pool_reset
bucket : ^PoolBucket = bucket_list.first
for ; bucket != nil; bucket = bucket.next
{
start := uintptr( bucket.blocks )
end := start + uintptr(bucket_capacity)
block_address := uintptr(raw_data(block))
if start <= block_address && block_address < end
{
misalignment := (block_address - start) % uintptr(block_size)
if misalignment != 0 {
ensure(false, "pool_validate_ownership: This data is within this pool's buckets, however its not aligned to the start of a block")
log(str_fmt("Block address: %p Misalignment: %p closest: %p",
transmute(rawptr)block_address,
transmute(rawptr)misalignment,
rawptr(block_address - misalignment)))
}
within_bucket = true
break
}
}
return within_bucket
}

View File

@ -0,0 +1,333 @@
/* Slab Allocator
These are a collection of pool allocators serving as a general way
to allocate a large amount of dynamic sized data.
The usual use case for this is an arena, stack,
or dedicated pool allocator fail to be enough to handle a data structure
that either is too random with its size (ex: strings)
or is intended to grow an abitrary degree with an unknown upper bound (dynamic arrays, and hashtables).
Technically speaking the general purpose situations can instead be grown on demand
with a dedicated segement of vmem, however this might be overkill
if the worst case buckets allocated are < 500 mb for most app usage.
The slab allocators are expected to hold growable pool allocators,
where each pool stores a 'bucket' of fixed-sized blocks of memory.
When a pools bucket is full it will request another bucket from its arena
for permanent usage within the arena's lifetime.
A freelist is tracked for free-blocks for each pool (provided by the underlying pool allocator)
A slab starts out with pools initialized with no buckets and grows as needed.
When a slab is initialized the slab policy is provided to know how many size-classes there should be
which each contain the ratio of bucket to block size.
*/
package grime
import "base:runtime"
import "core:mem"
import "core:slice"
SlabSizeClass :: struct {
bucket_capacity : uint,
block_size : uint,
block_alignment : uint,
}
Slab_Max_Size_Classes :: 64
SlabPolicy :: StackFixed(SlabSizeClass, Slab_Max_Size_Classes)
SlabHeader :: struct {
dbg_name : string,
tracker : MemoryTracker,
backing : Allocator,
pools : StackFixed(Pool, Slab_Max_Size_Classes),
}
Slab :: struct {
using header : ^SlabHeader,
}
slab_allocator :: proc( slab : Slab ) -> ( allocator : Allocator ) {
allocator.procedure = slab_allocator_proc
allocator.data = slab.header
return
}
slab_init :: proc( policy : ^SlabPolicy, bucket_reserve_num : uint = 0, allocator : Allocator = context.allocator, dbg_name : string = "", should_zero_buckets : b32 = false ) -> ( slab : Slab, alloc_error : AllocatorError )
{
header_size :: size_of( SlabHeader )
raw_mem : rawptr
raw_mem, alloc_error = alloc( header_size, mem.DEFAULT_ALIGNMENT, allocator )
if alloc_error != .None do return
slab.header = cast( ^SlabHeader) raw_mem
slab.backing = allocator
slab.dbg_name = dbg_name
when ODIN_DEBUG {
memtracker_init( & slab.tracker, allocator, Kilobyte * 256, dbg_name )
}
alloc_error = slab_init_pools( slab, policy, bucket_reserve_num, should_zero_buckets )
return
}
slab_init_pools :: proc ( using self : Slab, policy : ^SlabPolicy, bucket_reserve_num : uint = 0, should_zero_buckets : b32 ) -> AllocatorError
{
profile(#procedure)
for id in 0 ..< policy.idx {
using size_class := policy.items[id]
pool_dbg_name := str_fmt("%v pool[%v]", dbg_name, block_size, allocator = backing)
pool, alloc_error := pool_init( should_zero_buckets, block_size, bucket_capacity, bucket_reserve_num, block_alignment, backing, pool_dbg_name )
if alloc_error != .None do return alloc_error
push( & self.pools, pool )
}
return .None
}
slab_reload :: proc ( slab : Slab, allocator : Allocator )
{
slab.backing = allocator
for id in 0 ..< slab.pools.idx {
pool := slab.pools.items[id]
pool_reload( pool, slab.backing )
}
}
slab_destroy :: proc( using self : Slab )
{
for id in 0 ..< pools.idx {
pool := pools.items[id]
pool_destroy( pool )
}
free( self.header, backing )
when ODIN_DEBUG {
memtracker_clear(tracker)
}
}
slab_alloc :: proc( self : Slab,
size : uint,
alignment : uint,
zero_memory := true,
loc := #caller_location
) -> ( data : []byte, alloc_error : AllocatorError )
{
// profile(#procedure)
pool : Pool
id : u32 = 0
for ; id < self.pools.idx; id += 1 {
pool = self.pools.items[id]
if pool.block_size >= size && pool.alignment >= alignment {
break
}
}
verify( id < self.pools.idx, "There is not a size class in the slab's policy to satisfy the requested allocation", location = loc )
verify( pool.header != nil, "Requested alloc not supported by the slab allocator", location = loc )
block : []byte
slab_validate_pools( self )
block, alloc_error = pool_grab(pool)
slab_validate_pools( self )
if block == nil || alloc_error != .None {
ensure(false, "Bad block from pool")
return nil, alloc_error
}
// log( str_fmt_tmp("%v: Retrieved block: %p %d", self.dbg_name, raw_data(block), len(block) ))
data = byte_slice(raw_data(block), size)
if zero_memory {
slice.zero(data)
}
when ODIN_DEBUG {
memtracker_register_auto_name( & self.tracker, raw_data(block), & block[ len(block) - 1 ] )
}
return
}
slab_free :: proc( using self : Slab, data : []byte, loc := #caller_location )
{
// profile(#procedure)
pool : Pool
for id in 0 ..< pools.idx
{
pool = pools.items[id]
if pool_validate_ownership( pool, data ) {
start := raw_data(data)
end := ptr_offset(start, pool.block_size - 1)
when ODIN_DEBUG {
memtracker_unregister( self.tracker, { start, end } )
}
pool_release( pool, data, loc )
return
}
}
verify(false, "Attempted to free a block not within a pool of this slab", location = loc)
}
slab_resize :: proc( using self : Slab,
data : []byte,
new_size : uint,
alignment : uint,
zero_memory := true,
loc := #caller_location
) -> ( new_data : []byte, alloc_error : AllocatorError )
{
// profile(#procedure)
old_size := uint( len(data))
pool_resize, pool_old : Pool
for id in 0 ..< pools.idx
{
pool := pools.items[id]
if pool.block_size >= new_size && pool.alignment >= alignment {
pool_resize = pool
}
if pool_validate_ownership( pool, data ) {
pool_old = pool
}
if pool_resize.header != nil && pool_old.header != nil {
break
}
}
verify( pool_resize.header != nil, "Requested resize not supported by the slab allocator", location = loc )
// Resize will keep block in the same size_class, just give it more of its already allocated block
if pool_old.block_size == pool_resize.block_size
{
new_data_ptr := memory_after(data)
new_data = byte_slice( raw_data(data), new_size )
// log( dump_stacktrace() )
// log( str_fmt_tmp("%v: Resize via expanding block space allocation %p %d", dbg_name, new_data_ptr, int(new_size - old_size)))
if zero_memory && new_size > old_size {
to_zero := byte_slice( new_data_ptr, int(new_size - old_size) )
slab_validate_pools( self )
slice.zero( to_zero )
slab_validate_pools( self )
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", new_data_ptr, cast(rawptr) (uintptr(new_data_ptr) + uintptr(new_size - old_size))))
}
return
}
// We'll need to provide an entirely new block, so the data will need to be copied over.
new_block : []byte
slab_validate_pools( self )
new_block, alloc_error = pool_grab( pool_resize )
slab_validate_pools( self )
if new_block == nil {
ensure(false, "Retreived a null block")
return
}
if alloc_error != .None do return
// TODO(Ed): Reapply this when safe.
if zero_memory {
slice.zero( new_block )
// log( str_fmt_tmp("Zeroed memory - Range(%p to %p)", raw_data(new_block), cast(rawptr) (uintptr(raw_data(new_block)) + uintptr(new_size))))
}
// log( str_fmt_tmp("Resize via new block: %p %d (old : %p $d )", raw_data(new_block), len(new_block), raw_data(data), old_size ))
if raw_data(data) != raw_data(new_block) {
// log( str_fmt_tmp("%v: Resize via new block, copying from old data block to new block: (%p %d), (%p %d)", dbg_name, raw_data(data), len(data), raw_data(new_block), len(new_block)))
copy_non_overlapping( raw_data(new_block), raw_data(data), int(old_size) )
pool_release( pool_old, data )
start := raw_data( data )
end := rawptr(uintptr(start) + uintptr(pool_old.block_size) - 1)
when ODIN_DEBUG {
memtracker_unregister( self.tracker, { start, end } )
}
}
new_data = new_block[ :new_size]
when ODIN_DEBUG {
memtracker_register_auto_name( & self.tracker, raw_data(new_block), & new_block[ len(new_block) - 1 ] )
}
return
}
slab_reset :: proc( slab : Slab )
{
for id in 0 ..< slab.pools.idx {
pool := slab.pools.items[id]
pool_reset( pool )
}
when ODIN_DEBUG {
memtracker_clear(slab.tracker)
}
}
slab_validate_pools :: proc( slab : Slab )
{
when ! ODIN_DEBUG do return
slab := slab
for id in 0 ..< slab.pools.idx {
pool := slab.pools.items[id]
pool_validate( pool )
}
}
slab_allocator_proc :: proc(
allocator_data : rawptr,
mode : AllocatorMode,
size : int,
alignment : int,
old_memory : rawptr,
old_size : int,
loc := #caller_location
) -> ( data : []byte, alloc_error : AllocatorError)
{
slab : Slab
slab.header = cast( ^SlabHeader) allocator_data
size := uint(size)
alignment := uint(alignment)
old_size := uint(old_size)
switch mode
{
case .Alloc, .Alloc_Non_Zeroed:
return slab_alloc( slab, size, alignment, (mode != .Alloc_Non_Zeroed), loc)
case .Free:
slab_free( slab, byte_slice( old_memory, int(old_size)), loc )
case .Free_All:
slab_reset( slab )
case .Resize, .Resize_Non_Zeroed:
return slab_resize( slab, byte_slice(old_memory, int(old_size)), size, alignment, (mode != .Resize_Non_Zeroed), loc)
case .Query_Features:
set := cast( ^AllocatorModeSet) old_memory
if set != nil {
(set ^) = {.Alloc, .Alloc_Non_Zeroed, .Free_All, .Resize, .Query_Features}
}
case .Query_Info:
alloc_error = .Mode_Not_Implemented
}
return
}