diff --git a/src/base/hash/table/table.c b/src/base/hash/table/table.c new file mode 100644 index 0000000..59b9228 --- /dev/null +++ b/src/base/hash/table/table.c @@ -0,0 +1,248 @@ +// vim:fileencoding=utf-8:foldmethod=marker + +#include "table.h" +#include "../hasher/hasher.h" +#include "../../mem/allocator/mem_allocator.h" +#include "../../stream/stream.h" +#include "../../../common/aliases/aliases.h" +#include "../../../common/assert/assert.h" +#include "../../../common/misc/misc_utils.h" + +wp_persist inline void hashTableValidate(const WpHashTable *table, u64 key_size, u64 value_size); + +void *hashTableKeys(WpHashTable *table) { + hashTableValidate(table, 0, 0); + return table->keys; +} + +void *hashTableValues(WpHashTable *table) { + hashTableValidate(table, 0, 0); + return table->values; +} + +void *hashTableGetKeyFromBucket(WpHashTable *table, WpHashBucket bucket, u64 key_size) { + hashTableValidate(table, key_size, 0); + return arrayGet(table->keys, bucket.item_index, key_size); +} + +void *hashTableGetValueFromBucket(WpHashTable *table, WpHashBucket bucket, u64 value_size) { + hashTableValidate(table, 0, value_size); + return table->has_values ? arrayGet(table->values, bucket.item_index, value_size) : NULL; +} + +void *hashTableGetKeyFromBucketRef(WpHashTable *table, WpHashBucketRef ref, u64 key_size) { + return hashTableGetKeyFromBucket(table, table->buckets[ref.bucket_index], key_size); +} + +void *hashTableGetValueFromBucketRef(WpHashTable *table, WpHashBucketRef ref, u64 value_size) { + return hashTableGetValueFromBucket(table, table->buckets[ref.bucket_index], value_size); +} + +u64 hashTableCapacity(WpHashTable *table) { + hashTableValidate(table, 0, 0); + return wpArrayCapacity(table->keys); +} + +u64 hashTableCount(WpHashTable *table) { + hashTableValidate(table, 0, 0); + return wpArrayCount(table->keys); +} + +u64 hashTableKeySize(WpHashTable *table) { + hashTableValidate(table, 0, 0); + return wpArrayItemSize(table->keys); +} + +u64 hashTableValueSize(WpHashTable *table) { + hashTableValidate(table, 0, 0); + return table->has_values ? wpArrayItemSize(table->values) : 0; +} + +u64 hashTableCalcHash(WpHashTable *table, void *key, u64 key_size) { + hashTableValidate(table, key_size, 0); + wpDebugAssert(key != NULL, "`key` should not be NULL"); + WpU8Stream stream = table->encoder(key); + return wpHasherGetHash64(&table->hasher, &stream); +} + +void hashTableGrow(WpHashTable *table, const WpAllocator *allocator, u64 key_size, u64 value_size) { + hashTableValidate(table, key_size, value_size); + wpDebugAssert(allocator != NULL, "`allocator` should not be NULL"); + + u64 capacity = hashTableCapacity(table) * 2; + u64 mask = capacity - wpU64Const(1); + WpArray keys = arrayAllocCapacity(allocator, capacity, WP_ARRAY_INIT_NONE, key_size); + WpArray values = NULL; + WpHashBucketArray buckets = wpArrayAllocCapacity(WpHashBucket, allocator, capacity, WP_ARRAY_INIT_FILLED); + + if (table->has_values) { + values = arrayAllocCapacity(allocator, capacity, WP_ARRAY_INIT_NONE, value_size); + } + + WpArray old_keys = table->keys; + WpArray old_values = table->values; + + table->keys = keys; + table->values = values; + table->buckets = buckets; + table->bucket_mask = mask; + + for (u64 i = 0; i < wpArrayCount(old_keys); ++i) { + void *key = arrayGet(old_keys, i, key_size); + void *value = table->has_values ? arrayGet(old_values, i, value_size) : NULL; + u64 hash = hashTableCalcHash(table, key, key_size); + WpHashBucketRef ref = hashTableBucketRef(table, hash); + hashTableInsert(table, key, value, ref, key_size, value_size); + } +} + +WpHashLookupResult hashTableLookup(WpHashTable *table, void *key, WpHashBucketRef ref, u64 key_size) { + hashTableValidate(table, key_size, 0); + wpDebugAssert(key != NULL, "`key` should not be NULL"); + + while (true) { + WpHashBucket *bucket = &table->buckets[ref.bucket_index]; + + if (ref.dist_and_tag == bucket->dist_and_tag) { + b8 key_equal = table->key_eq(key, arrayGet(table->keys, bucket->item_index, key_size)); + return (WpHashLookupResult){ + .ref.dist_and_tag = ref.dist_and_tag, + .ref.bucket_index = ref.bucket_index, + .found = key_equal, + }; + } else if (hashTableGetDist(ref.dist_and_tag) > hashTableGetDist(bucket->dist_and_tag)) { + return (WpHashLookupResult){ + .ref.dist_and_tag = ref.dist_and_tag, + .ref.bucket_index = ref.bucket_index, + .found = false, + }; + } + + ref.dist_and_tag = hashTableDistInc(ref.dist_and_tag); + ref.bucket_index = hashTableNextBucket(table, ref.bucket_index); + } +} + +void hashTableUpdate(WpHashTable *table, void *value, WpHashBucketRef ref, u64 value_size) { + hashTableValidate(table, 0, value_size); + if (!(table->has_values)) { + wpRuntimeAssert(0, "Attempting to update a hash table that has no values"); + } + wpDebugAssert(value != NULL, "`value` should not be NULL"); + + WpHashBucket *bucket = &table->buckets[ref.bucket_index]; + arraySet(table->values, bucket->item_index, value, value_size); +} + +void hashTableInsert(WpHashTable *table, void *key, void *value, WpHashBucketRef ref, u64 key_size, + u64 value_size) { + hashTableValidate(table, key_size, value_size); + wpDebugAssert(key != NULL, "`key` should not be NULL"); + + u64 index = wpArrayCount(table->keys); + while (true) { + WpHashBucket *bucket = &table->buckets[ref.bucket_index]; + + if (bucket->dist_and_tag == 0) { + bucket->dist_and_tag = ref.dist_and_tag; + bucket->item_index = index; + break; + } else if (hashTableGetDist(ref.dist_and_tag) > hashTableGetDist(bucket->dist_and_tag)) { + wpMiscUtilsSwap(u64, &ref.dist_and_tag, &bucket->dist_and_tag); + wpMiscUtilsSwap(u64, &index, &bucket->item_index); + } + + ref.dist_and_tag = hashTableDistInc(ref.dist_and_tag); + ref.bucket_index = hashTableNextBucket(table, ref.bucket_index); + } + + arrayAppendCapped(table->keys, key, key_size); + if (table->has_values) { + arrayAppendCapped(table->values, value, value_size); + } + table->load_factor = (f32)wpArrayCount(table->keys) / (f32)wpArrayCapacity(table->keys); +} + +void hashTableRemove(WpHashTable *table, WpHashBucketRef ref, u64 key_size, u64 value_size) { + hashTableValidate(table, key_size, value_size); + + WpHashBucket *bucket = &table->buckets[ref.bucket_index]; + + u64 last_item_index = wpArrayCount(table->keys) - 1; + + if (bucket->item_index < last_item_index) { + // Update the bucket for the last item before swapping + u64 hash = hashTableCalcHash(table, arrayGet(table->keys, last_item_index, key_size), key_size); + + WpHashLookupResult result = hashTableLookup(table, arrayGet(table->keys, last_item_index, key_size), + hashTableBucketRef(table, hash), key_size); + wpRuntimeAssert(result.found, "Hash table item should exist but it doesn't"); + + table->buckets[result.ref.bucket_index].item_index = bucket->item_index; + + arrayRemoveSwapEnd(table->keys, bucket->item_index, key_size); + if (table->has_values) { + arrayRemoveSwapEnd(table->values, bucket->item_index, value_size); + } + } + + while (bucket->dist_and_tag > 0) { + u64 next_index = hashTableNextBucket(table, ref.bucket_index); + WpHashBucket *next = &table->buckets[next_index]; + + if (hashTableGetDist(next->dist_and_tag) > WP_HASH_DIST_INC) { + bucket->dist_and_tag = hashTableDistDec(next->dist_and_tag); + bucket->item_index = next->item_index; + + ref.bucket_index = next_index; + bucket = &table->buckets[ref.bucket_index]; + } else { + bucket->dist_and_tag = 0; + bucket->item_index = 0; + } + } +} + +void hashTableClear(WpHashTable *table, u64 key_size, u64 value_size) { + hashTableValidate(table, key_size, value_size); + arrayClear(table->keys, key_size); + if (table->has_values) { + arrayClear(table->values, value_size); + } + wpArrayZero(WpHashBucket, table->buckets); +} + +WpHashBucketRef hashTableBucketRef(WpHashTable *table, u64 hash) { + hashTableValidate(table, 0, 0); + return (WpHashBucketRef){ + .dist_and_tag = WP_HASH_DIST_INC | (hash & WP_HASH_TAG_MASK), + .bucket_index = hash & table->bucket_mask, + }; +} + +u64 hashTableGetDist(u64 dist_and_tag) { + return dist_and_tag & (~WP_HASH_TAG_MASK); +} + +u64 hashTableNextBucket(WpHashTable *table, u64 bucket_index) { + return (++bucket_index) & table->bucket_mask; +} + +u64 hashTableDistInc(u64 dist_and_tag) { + return dist_and_tag + WP_HASH_DIST_INC; +} + +u64 hashTableDistDec(u64 dist_and_tag) { + return dist_and_tag - WP_HASH_DIST_INC; +} + +wp_persist inline void hashTableValidate(const WpHashTable *table, u64 key_size, u64 value_size) { + wpDebugAssert(table != NULL, "`table` should not be NULL"); + wpRuntimeAssert(WP_HASH_TABLE_MAGIC == table->magic, "`table` is not a valid wapp hash table"); + if (key_size > 0) { + wpRuntimeAssert(key_size == wpArrayItemSize(table->keys), "Invalid key type provided"); + } + if (value_size > 0) { + wpRuntimeAssert(value_size == wpArrayItemSize(table->values), "Invalid value type provided"); + } +} diff --git a/src/base/hash/table/table.h b/src/base/hash/table/table.h new file mode 100644 index 0000000..4ac5279 --- /dev/null +++ b/src/base/hash/table/table.h @@ -0,0 +1,99 @@ +// vim:fileencoding=utf-8:foldmethod=marker + +/** + * A hash table implementation with Robin Hood open addressing for collision resolution. + * + * The implementation is based on a version of this C++ implementation: + * https://github.com/martinus/unordered_dense + * + * Extra references: + * - https://faultlore.com/blah/robinhood-part-1/ + * - https://blog.hieunt.me/blog/robinhood-hashing + * + * IMPORTANT: + * This is the internal hash table implementation and it is not intended to be used directly. + * Please check ../map/map.h and ../set/set.h for the public APIs. + */ + +#ifndef TABLE_H +#define TABLE_H + +#include "../hasher/hasher.h" +#include "../../array/array.h" +#include "../../mem/allocator/mem_allocator.h" +#include "../../stream/stream.h" +#include "../../../common/aliases/aliases.h" +#include "../../../common/platform/platform.h" + +#ifdef WP_PLATFORM_CPP +BEGIN_C_LINKAGE +#endif // !WP_PLATFORM_CPP + +#define WP_HASH_TABLE_MAGIC wpU64Const(0x57504854424c) +#define WP_HASH_TAG_BITS wpU64Const(32) +#define WP_HASH_DIST_INC (wpU64Const(1) << WP_HASH_TAG_BITS) +#define WP_HASH_TAG_MASK (WP_HASH_DIST_INC - wpU64Const(1)) +#define WP_HASH_MAX_LOAD_FACTOR 0.65f + +typedef WpU8Stream (*WpU8StreamEncoder)(void *data); +typedef b8 (*WpKeyEqualTest)(void *a, void *b); + +typedef struct WpHashBucket { + u64 dist_and_tag; + u64 item_index; +} WpHashBucket; +typedef WpHashBucket *WpHashBucketArray; + +typedef struct WpHashTable { + WpHasher hasher; + WpU8StreamEncoder encoder; + WpKeyEqualTest key_eq; + WpArray keys; + WpArray values; + WpHashBucketArray buckets; + u64 magic; + u64 bucket_mask; + f32 load_factor; + b8 has_values; +} WpHashTable; + +typedef struct WpHashBucketRef { + u64 dist_and_tag; + u64 bucket_index; +} WpHashBucketRef; + +typedef struct WpHashLookupResult { + WpHashBucketRef ref; + b8 found; +} WpHashLookupResult; + +void *hashTableKeys(WpHashTable *table); +void *hashTableValues(WpHashTable *table); +void *hashTableGetKeyFromBucket(WpHashTable *table, WpHashBucket bucket, u64 key_size); +void *hashTableGetValueFromBucket(WpHashTable *table, WpHashBucket bucket, u64 value_size); +void *hashTableGetKeyFromBucketRef(WpHashTable *table, WpHashBucketRef ref, u64 key_size); +void *hashTableGetValueFromBucketRef(WpHashTable *table, WpHashBucketRef ref, u64 value_size); +u64 hashTableCapacity(WpHashTable *table); +u64 hashTableCount(WpHashTable *table); +u64 hashTableKeySize(WpHashTable *table); +u64 hashTableValueSize(WpHashTable *table); +u64 hashTableCalcHash(WpHashTable *table, void *key, u64 key_size); +void hashTableGrow(WpHashTable *table, const WpAllocator *allocator, u64 key_size, + u64 value_size); +WpHashLookupResult hashTableLookup(WpHashTable *table, void *key, WpHashBucketRef ref, u64 key_size); +void hashTableUpdate(WpHashTable *table, void *value, WpHashBucketRef ref, u64 value_size); +void hashTableInsert(WpHashTable *table, void *key, void *value, WpHashBucketRef ref, + u64 key_size, u64 value_size); +void hashTableRemove(WpHashTable *table, WpHashBucketRef ref, u64 key_size, u64 value_size); +void hashTableClear(WpHashTable *table, u64 key_size, u64 value_size); +WpHashBucketRef hashTableBucketRef(WpHashTable *table, u64 hash); +u64 hashTableGetDist(u64 dist_and_tag); +u64 hashTableNextBucket(WpHashTable *table, u64 bucket_index); +u64 hashTableDistInc(u64 dist_and_tag); +u64 hashTableDistDec(u64 dist_and_tag); + +#ifdef WP_PLATFORM_CPP +END_C_LINKAGE +#endif // !WP_PLATFORM_CPP + +#endif // !TABLE_H diff --git a/src/base/wapp_base.c b/src/base/wapp_base.c index 15a4bc6..297a368 100644 --- a/src/base/wapp_base.c +++ b/src/base/wapp_base.c @@ -9,6 +9,7 @@ #include "hash/hasher/hasher.c" #include "hash/hasher/murmur3.c" #include "hash/hasher/siphash.c" +#include "hash/table/table.c" #include "queue/queue.c" #include "stream/stream.c" #include "mem/allocator/mem_allocator.c" diff --git a/src/base/wapp_base.h b/src/base/wapp_base.h index 894349d..940bb23 100644 --- a/src/base/wapp_base.h +++ b/src/base/wapp_base.h @@ -8,6 +8,7 @@ #include "hash/hasher/hasher.h" #include "hash/hasher/murmur3.h" #include "hash/hasher/siphash.h" +#include "hash/table/table.h" #include "queue/queue.h" #include "stream/stream.h" #include "mem/allocator/mem_allocator.h"