Files
VEFontCache-Odin/vefontcache/LRU.odin

288 lines
7.9 KiB
Odin

package vefontcache
/* Note(Ed):
Original implementation has been changed moderately.
Notably the LRU is now type generic for its key value.
This was done to profile between using u64, u32, and u16.
What ended up happening was using u32 for both the atlas and the shape cache
yielded a several ms save for processing thousands of draw text calls.
There was an attempt at an optimization pass but the directives done here (other than force_inline)
are marginal changes at best.
*/
// 16-bit hashing was attempted, however it seems to get collisions with djb8_hash_16
LRU_Fail_Mask_16 :: 0xFFFF
LRU_Fail_Mask_32 :: 0xFFFFFFFF
LRU_Fail_Mask_64 :: 0xFFFFFFFFFFFFFFFF
Pool_ListIter :: i32
Pool_List_Item :: struct( $V_Type : typeid ) #packed {
// Pool_List_Item :: struct( $V_Type : typeid ) {
prev : Pool_ListIter,
next : Pool_ListIter,
value : V_Type,
}
Pool_List :: struct( $V_Type : typeid) {
items : [dynamic]Pool_List_Item(V_Type),
free_list : [dynamic]Pool_ListIter,
front : Pool_ListIter,
back : Pool_ListIter,
size : i32,
capacity : i32,
dbg_name : string,
}
pool_list_init :: proc( pool : ^Pool_List($V_Type), capacity : i32, dbg_name : string = "" )
{
error : Allocator_Error
pool.items, error = make( [dynamic]Pool_List_Item(V_Type), int(capacity) )
assert( error == .None, "VEFontCache.pool_list_inits: Failed to allocate items array")
resize( & pool.items, capacity )
pool.free_list, error = make( [dynamic]Pool_ListIter, len = 0, cap = int(capacity) )
assert( error == .None, "VEFontCache.pool_list_init: Failed to allocate free_list array")
resize( & pool.free_list, capacity )
pool.capacity = capacity
pool.dbg_name = dbg_name
for id in 0 ..< pool.capacity {
pool.free_list[id] = Pool_ListIter(id)
pool.items[id] = {
prev = -1,
next = -1,
}
}
pool.front = -1
pool.back = -1
}
pool_list_free :: proc( pool : ^Pool_List($V_Type) ) {
delete( pool.items)
delete( pool.free_list)
}
pool_list_reload :: proc( pool : ^Pool_List($V_Type), allocator : Allocator ) {
reload_array( & pool.items, allocator )
reload_array( & pool.free_list, allocator )
}
pool_list_clear :: proc( pool: ^Pool_List($V_Type) )
{
clear(& pool.items)
clear(& pool.free_list)
resize( & pool.items, cap(pool.items) )
resize( & pool.free_list, cap(pool.free_list) )
for id in 0 ..< pool.capacity {
pool.free_list[id] = Pool_ListIter(id)
pool.items[id] = {
prev = -1,
next = -1,
}
}
pool.front = -1
pool.back = -1
pool.size = 0
}
@(optimization_mode="favor_size")
pool_list_push_front :: proc( pool : ^Pool_List($V_Type), value : V_Type ) #no_bounds_check
{
if pool.size >= pool.capacity do return
length := len(pool.free_list)
assert( length > 0 )
assert( length == int(pool.capacity - pool.size) )
id := pool.free_list[ len(pool.free_list) - 1 ]
// if pool.dbg_name != "" {
// logf("pool_list: back %v", id)
// }
pop( & pool.free_list )
pool.items[ id ].prev = -1
pool.items[ id ].next = pool.front
pool.items[ id ].value = value
// if pool.dbg_name != "" {
// logf("pool_list: pushed %v into id %v", value, id)
// }
if pool.front != -1 do pool.items[ pool.front ].prev = id
if pool.back == -1 do pool.back = id
pool.front = id
pool.size += 1
}
@(optimization_mode="favor_size")
pool_list_erase :: proc( pool : ^Pool_List($V_Type), iter : Pool_ListIter ) #no_bounds_check
{
if pool.size <= 0 do return
assert( iter >= 0 && iter < Pool_ListIter(pool.capacity) )
assert( len(pool.free_list) == int(pool.capacity - pool.size) )
iter_node := & pool.items[ iter ]
prev := iter_node.prev
next := iter_node.next
if iter_node.prev != -1 do pool.items[ prev ].next = iter_node.next
if iter_node.next != -1 do pool.items[ next ].prev = iter_node.prev
if pool.front == iter do pool.front = iter_node.next
if pool.back == iter do pool.back = iter_node.prev
iter_node.prev = -1
iter_node.next = -1
iter_node.value = 0
append( & pool.free_list, iter )
pool.size -= 1
if pool.size == 0 {
pool.back = -1
pool.front = -1
}
}
@(optimization_mode="favor_size")
pool_list_move_to_front :: proc "contextless" ( pool : ^Pool_List($V_Type), iter : Pool_ListIter ) #no_bounds_check
{
if pool.front == iter do return
item := & pool.items[iter]
if item.prev != -1 do pool.items[ item.prev ].next = item.next
if item.next != -1 do pool.items[ item.next ].prev = item.prev
if pool.back == iter do pool.back = item.prev
item.prev = -1
item.next = pool.front
pool.items[ pool.front ].prev = iter
pool.front = iter
}
@(optimization_mode="favor_size")
pool_list_peek_back :: #force_inline proc ( pool : Pool_List($V_Type) ) -> V_Type #no_bounds_check {
assert( pool.back != - 1 )
value := pool.items[ pool.back ].value
return value
}
@(optimization_mode="favor_size")
pool_list_pop_back :: #force_inline proc( pool : ^Pool_List($V_Type) ) -> V_Type #no_bounds_check {
if pool.size <= 0 do return 0
assert( pool.back != -1 )
value := pool.items[ pool.back ].value
pool_list_erase( pool, pool.back )
return value
}
LRU_Link :: struct #packed {
value : i32,
ptr : Pool_ListIter,
}
LRU_Cache :: struct( $Key_Type : typeid ) {
capacity : i32,
num : i32,
table : map[Key_Type]LRU_Link,
key_queue : Pool_List(Key_Type),
}
lru_init :: proc( cache : ^LRU_Cache($Key_Type), capacity : i32, dbg_name : string = "" ) {
error : Allocator_Error
cache.capacity = capacity
cache.table, error = make( map[Key_Type]LRU_Link, uint(capacity) )
assert( error == .None, "VEFontCache.lru_init : Failed to allocate cache's table")
pool_list_init( & cache.key_queue, capacity, dbg_name = dbg_name )
}
lru_free :: proc( cache : ^LRU_Cache($Key_Type) ) {
pool_list_free( & cache.key_queue )
delete( cache.table )
}
lru_reload :: #force_inline proc( cache : ^LRU_Cache($Key_Type), allocator : Allocator ) {
reload_map( & cache.table, allocator )
pool_list_reload( & cache.key_queue, allocator )
}
lru_clear :: proc ( cache : ^LRU_Cache($Key_Type) ) {
pool_list_clear( & cache.key_queue )
clear(& cache.table)
cache.num = 0
}
@(optimization_mode="favor_size")
lru_find :: #force_inline proc "contextless" ( cache : LRU_Cache($Key_Type), key : Key_Type, must_find := false ) -> (LRU_Link, bool) #no_bounds_check {
link, success := cache.table[key]
return link, success
}
@(optimization_mode="favor_size")
lru_get :: #force_inline proc ( cache: ^LRU_Cache($Key_Type), key : Key_Type ) -> i32 #no_bounds_check {
if link, ok := &cache.table[ key ]; ok {
pool_list_move_to_front(&cache.key_queue, link.ptr)
return link.value
}
return -1
}
@(optimization_mode="favor_size")
lru_get_next_evicted :: #force_inline proc ( cache : LRU_Cache($Key_Type) ) -> Key_Type #no_bounds_check {
if cache.key_queue.size >= cache.capacity {
evict := pool_list_peek_back( cache.key_queue )
return evict
}
return ~Key_Type(0)
}
@(optimization_mode="favor_size")
lru_peek :: #force_inline proc "contextless" ( cache : LRU_Cache($Key_Type), key : Key_Type, must_find := false ) -> i32 #no_bounds_check {
iter, success := lru_find( cache, key, must_find )
if success == false {
return -1
}
return iter.value
}
@(optimization_mode="favor_size")
lru_put :: proc( cache : ^LRU_Cache($Key_Type), key : Key_Type, value : i32 ) -> Key_Type #no_bounds_check
{
// profile(#procedure)
if link, ok := & cache.table[ key ]; ok {
pool_list_move_to_front( & cache.key_queue, link.ptr )
link.value = value
return key
}
evict := key
if cache.key_queue.size >= cache.capacity {
evict = pool_list_pop_back(&cache.key_queue)
delete_key(&cache.table, evict)
cache.num -= 1
}
pool_list_push_front(&cache.key_queue, key)
cache.table[key] = LRU_Link{
value = value,
ptr = cache.key_queue.front,
}
cache.num += 1
return evict
}
lru_refresh :: proc( cache : ^LRU_Cache($Key_Type), key : Key_Type ) {
link, success := lru_find( cache ^, key )
pool_list_erase( & cache.key_queue, link.ptr )
pool_list_push_front( & cache.key_queue, key )
link.ptr = cache.key_queue.front
}