Naive hash table & tests

This commit is contained in:
Charlie 2024-07-06 15:50:16 +02:00
parent 05919248eb
commit ebaa52c326
8 changed files with 388 additions and 18 deletions

View file

@ -3,7 +3,7 @@
/// ///
// Build config stuff // Build config stuff
#define RUN_TESTS 0 #define RUN_TESTS 1
// This is only for people developing oogabooga! // This is only for people developing oogabooga!
#define OOGABOOGA_DEV 1 #define OOGABOOGA_DEV 1
@ -11,7 +11,7 @@
#define ENABLE_PROFILING 0 #define ENABLE_PROFILING 0
// ENABLE_SIMD Requires CPU to support at least SSE1 but I will be very surprised if you find a system today which doesn't // ENABLE_SIMD Requires CPU to support at least SSE1 but I will be very surprised if you find a system today which doesn't
#define ENABLE_SIMD 1 #define ENABLE_SIMD 0
#define INITIAL_PROGRAM_MEMORY_SIZE MB(5) #define INITIAL_PROGRAM_MEMORY_SIZE MB(5)
@ -40,10 +40,10 @@ typedef struct Context_Extra {
// #include "oogabooga/examples/minimal_game_loop.c" // #include "oogabooga/examples/minimal_game_loop.c"
// An engine dev stress test for rendering // An engine dev stress test for rendering
// #include "oogabooga/examples/renderer_stress_test.c" #include "oogabooga/examples/renderer_stress_test.c"
// Randy's example game that he's building out as a tutorial for using the engine // Randy's example game that he's building out as a tutorial for using the engine
#include "entry_randygame.c" // #include "entry_randygame.c"
// This is where you swap in your own project! // This is where you swap in your own project!
// #include "entry_yourepicgamename.c" // #include "entry_yourepicgamename.c"

View file

@ -118,3 +118,19 @@ void pop_context() {
} }
u64 get_next_power_of_two(u64 x) {
if (x == 0) {
return 1;
}
x--;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x |= x >> 32;
return x + 1;
}

View file

@ -76,7 +76,7 @@ int entry(int argc, char **argv) {
draw_frame.view = camera_view; draw_frame.view = camera_view;
seed_for_random = 69; seed_for_random = 69;
for (u64 i = 0; i < 100000; i++) { for (u64 i = 0; i < 10000; i++) {
float32 aspect = (float32)window.width/(float32)window.height; float32 aspect = (float32)window.width/(float32)window.height;
float min_x = -aspect; float min_x = -aspect;
float max_x = aspect; float max_x = aspect;
@ -102,11 +102,13 @@ int entry(int argc, char **argv) {
draw_image(bush_image, v2(0.65, 0.65), v2(0.2*sin(now), 0.2*sin(now)), COLOR_WHITE); draw_image(bush_image, v2(0.65, 0.65), v2(0.2*sin(now), 0.2*sin(now)), COLOR_WHITE);
//draw_text(font, "I am text", v2(0.1, 0.6), COLOR_BLACK);
//draw_text(font, "I am text", v2(0.09, 0.61), COLOR_WHITE);
tm_scope_cycles("gfx_update") { tm_scope_cycles("gfx_update") {
gfx_update(); gfx_update();
} }
if (is_key_just_released('E')) { if (is_key_just_released('E')) {
log("FPS: %.2f", 1.0 / delta); log("FPS: %.2f", 1.0 / delta);
log("ms: %.2f", delta*1000.0); log("ms: %.2f", delta*1000.0);

84
oogabooga/hash.c Normal file
View file

@ -0,0 +1,84 @@
#define PRIME64_1 11400714785074694791ULL
#define PRIME64_2 14029467366897019727ULL
#define PRIME64_3 1609587929392839161ULL
#define PRIME64_4 9650029242287828579ULL
#define PRIME64_5 2870177450012600261ULL
static inline u64 xx_hash(u64 x) {
u64 h64 = PRIME64_5 + 8;
h64 += x * PRIME64_3;
h64 = ((h64 << 23) | (h64 >> (64 - 23))) * PRIME64_2 + PRIME64_4;
h64 ^= h64 >> 33;
h64 *= PRIME64_2;
h64 ^= h64 >> 29;
h64 *= PRIME64_3;
h64 ^= h64 >> 32;
return h64;
}
static inline u64 city_hash(string s) {
const u64 k = 0x9ddfea08eb382d69ULL;
u64 a = s.count;
u64 b = s.count * 5;
u64 c = 9;
u64 d = b;
if (s.count <= 16) {
memcpy(&a, s.data, sizeof(u64));
memcpy(&b, s.data + s.count - 8, sizeof(u64));
} else {
memcpy(&a, s.data, sizeof(u64));
memcpy(&b, s.data + 8, sizeof(u64));
memcpy(&c, s.data + s.count - 8, sizeof(u64));
memcpy(&d, s.data + s.count - 16, sizeof(u64));
}
a += b;
a = (a << 43) | (a >> (64 - 43));
a += c;
a = a * 5 + 0x52dce729;
d ^= a;
d = (d << 44) | (d >> (64 - 44));
d += b;
return d * k;
}
u64 djb2_hash(string s) {
u64 hash = 5381;
for (u64 i = 0; i < s.count; i++) {
hash = ((hash << 5) + hash) + s.data[i];
}
return hash;
}
u64 string_get_hash(string s) {
if (s.count > 32) return djb2_hash(s);
return city_hash(s);
}
u64 pointer_get_hash(void *p) {
return xx_hash((u64)p);
}
u64 float64_get_hash(float64 x) {
return xx_hash(*(u64*)&x);
}
u64 float32_get_hash(float32 x) {
return float64_get_hash((float64)x);
}
#define get_hash(x) _Generic((x), \
string: string_get_hash, \
s8: xx_hash, \
u8: xx_hash, \
s16: xx_hash, \
u16: xx_hash, \
s32: xx_hash, \
u32: xx_hash, \
s64: xx_hash, \
u64: xx_hash, \
f32: float32_get_hash, \
f64: float64_get_hash, \
default: pointer_get_hash \
)(x)

208
oogabooga/hash_table.c Normal file
View file

@ -0,0 +1,208 @@
// Very naive implementation for now but it should be cache efficient so not really a
// problem until very large (in which case you should probably write your own data structure
// anyways)
/*
Example Usage:
// Make a table with key type 'string' and value type 'int', allocated on the heap
Hash_Table table = make_hash_table(string, int, get_heap_allocator());
// Set key "Key string" to integer value 69. This returns whether or not key was newly added.
string key = STR("Key string");
bool newly_added = hash_table_set(&table, key, 69);
// Find value associated with given key. Returns pointer to that value.
string other_key = STR("Some other key");
int* value = hash_table_find(&table, other_key);
if (value) {
// Pointer is OK, item with key exists
} else {
// Pointer is null, item with key does NOT exist
}
// Same as hash_table_find() != NULL
string another_key = STR("Another key");
if (hash_table_contains(&table, another_key)) {
}
// Reset all entries (but keep allocated memory)
hash_table_reset(&table);
// Free allocated entries in hash table
hash_table_destroy(&table);
Limitations:
- Key can only be a base type or pointer
- Key and value passed to the following function needs to be lvalues (we need to be able to take their addresses with '&'):
- hash_table_add
- hash_table_find
- hash_table_contains
- hash_table_set
Example:
hash_table_set(&table, my_key+5, my_value+3); // ERROR
int key = my_key+5;
int value = my_value+3;
hash_table_set(&table, key, value); // OK
*/
typedef struct Hash_Table Hash_Table;
// API:
#define make_hash_table_reserve(Key_Type, Value_Type, capacity_count, allocator) \
make_hash_table_reserve(sizeof(Key_Type), sizeof(Value_Type), capacity_count, allocator)
#define make_hash_table(Key_Type, Value_Type, allocator) \
make_hash_table_raw(sizeof(Key_Type), sizeof(Value_Type), allocator)
#define hash_table_add(table_ptr, key, value) \
hash_table_add_raw((table_ptr), get_hash(key), &(key), &(value), sizeof(key), sizeof(value))
#define hash_table_find(table_ptr, key) \
hash_table_find_raw((table_ptr), get_hash(key))
#define hash_table_contains(table_ptr, key) \
hash_table_contains_raw((table_ptr), get_hash(key))
#define hash_table_set(table_ptr, key, value) \
hash_table_set_raw((table_ptr), get_hash(key), &key, &value, sizeof(key), sizeof(value))
void hash_table_reserve(Hash_Table *t, u64 required_count);
typedef struct Hash_Table {
// Each entry is hash-key-value
// Hash is sizeof(u64) bytes, key is _key_size bytes and value is _value_size bytes
void *entries;
u64 count; // Number of valid entries
u64 capacity_count; // Number of allocated entries
u64 _key_size;
u64 _value_size;
Allocator allocator;
} Hash_Table;
Hash_Table make_hash_table_reserve_raw(u64 key_size, u64 value_size, u64 capacity_count, Allocator allocator) {
capacity_count = min(capacity_count, 8);
Hash_Table t = ZERO(Hash_Table);
t._key_size = key_size;
t._value_size = value_size;
t.allocator = allocator;
u64 entry_size = value_size+sizeof(u64);
t.entries = alloc(t.allocator, entry_size*capacity_count);
memset(t.entries, 0, entry_size*capacity_count);
t.capacity_count = capacity_count;
return t;
}
inline Hash_Table make_hash_table_raw(u64 key_size, u64 value_size, Allocator allocator) {
return make_hash_table_reserve_raw(key_size, value_size, 128, allocator);
}
void hash_table_reset(Hash_Table *t) {
t->count = 0;
}
void hash_table_destroy(Hash_Table *t) {
dealloc(t->allocator, t->entries);
t->entries = 0;
t->count = 0;
t->capacity_count = 0;
}
void hash_table_reserve(Hash_Table *t, u64 required_count) {
u64 entry_size = t->_value_size+sizeof(u64);
u64 required_size = required_count*entry_size;
u64 current_size = t->capacity_count*entry_size;
if (current_size >= required_size) return;
u64 new_count = get_next_power_of_two(required_count);
u64 new_size = new_count*entry_size;
void *new_entries = alloc(t->allocator, new_size);
memcpy(new_entries, t->entries, current_size);
dealloc(t->allocator, t->entries);
t->entries = new_entries;
t->capacity_count = new_count;
}
// This can add multiple entries of same hash, beware!
void hash_table_add_raw(Hash_Table *t, u64 hash, void *k, void *v, u64 key_size, u64 value_size) {
assert(t->_key_size == key_size, "Key type size does not match hash table initted key type size");
assert(t->_value_size == value_size, "Value type size does not match hash table initted value type size");
hash_table_reserve(t, t->count+1);
u64 entry_size = t->_value_size+sizeof(u64);
u64 index = entry_size*t->count;
t->count += 1;
u64 hash_offset = 0;
u64 value_offset = hash_offset + sizeof(u64);
memcpy((u8*)t->entries+index+hash_offset, &hash, sizeof(u64));
memcpy((u8*)t->entries+index+value_offset, v, value_size);
}
void *hash_table_find_raw(Hash_Table *t, u64 hash) {
// #Speed #Incomplete
// Do quadratic probe 'triangular numbers'
u64 entry_size = t->_value_size+sizeof(u64);
u64 hash_offset = 0;
u64 value_offset = hash_offset + sizeof(u64);
for (u64 i = 0; i < t->count; i += 1) {
u64 existing_hash = *(u64*)((u8*)t->entries+i*entry_size+hash_offset);
if (existing_hash == hash) {
void *value = ((u8*)t->entries+i*entry_size+value_offset);
return value;
}
}
return 0;
}
bool hash_table_contains_raw(Hash_Table *t, u64 hash) {
return hash_table_find_raw(t, hash) != 0;
}
// Returns true if key was newly added or false if it already existed
bool hash_table_set_raw(Hash_Table *t, u64 hash, void *k, void *v, u64 key_size, u64 value_size) {
bool newly_added = true;
if (hash_table_contains_raw(t, hash)) newly_added = false;
if (newly_added) {
hash_table_add_raw(t, hash, k, v, key_size, value_size);
} else {
memcpy(hash_table_find_raw(t, hash), v, value_size);
}
return newly_added;
}

View file

@ -197,7 +197,7 @@ Heap_Block *make_heap_block(Heap_Block *parent, u64 size) {
// #Speed #Cleanup // #Speed #Cleanup
if (((u8*)block)+size >= ((u8*)program_memory)+program_memory_size) { if (((u8*)block)+size >= ((u8*)program_memory)+program_memory_size) {
u64 minimum_size = ((u8*)block+size) - (u8*)program_memory + 1; u64 minimum_size = ((u8*)block+size) - (u8*)program_memory + 1;
u64 new_program_size = max((cast(u64)(minimum_size*2)), program_memory_size*2); u64 new_program_size = get_next_power_of_two(minimum_size);
assert(new_program_size >= minimum_size, "Bröd"); assert(new_program_size >= minimum_size, "Bröd");
const u64 ATTEMPTS = 1000; const u64 ATTEMPTS = 1000;
for (u64 i = 0; i <= ATTEMPTS; i++) { for (u64 i = 0; i <= ATTEMPTS; i++) {

View file

@ -253,9 +253,12 @@ typedef u8 bool;
#include "string.c" #include "string.c"
#include "unicode.c" #include "unicode.c"
#include "string_format.c" #include "string_format.c"
#include "hash.c"
#include "path_utils.c" #include "path_utils.c"
#include "linmath.c" #include "linmath.c"
#include "hash_table.c"
#include "os_interface.c" #include "os_interface.c"
#include "gfx_interface.c" #include "gfx_interface.c"
@ -291,6 +294,8 @@ typedef u8 bool;
#include "tests.c" #include "tests.c"
#define malloc please_use_alloc_for_memory_allocations_instead_of_malloc
#define free please_use_dealloc_for_memory_deallocations_instead_of_free
void oogabooga_init(u64 program_memory_size) { void oogabooga_init(u64 program_memory_size) {
context.logger = default_logger; context.logger = default_logger;

View file

@ -36,6 +36,8 @@ void log_heap() {
void test_allocator(bool do_log_heap) { void test_allocator(bool do_log_heap) {
u64 h = get_hash((string*)69);
Allocator heap = get_heap_allocator(); Allocator heap = get_heap_allocator();
// Basic allocation and free // Basic allocation and free
@ -1018,6 +1020,51 @@ void test_linmath() {
assert(floats_roughly_match(v3_dot, 38), "Failed: v3_dot_product"); assert(floats_roughly_match(v3_dot, 38), "Failed: v3_dot_product");
assert(floats_roughly_match(v4_dot, 30), "Failed: v4_dot_product"); assert(floats_roughly_match(v4_dot, 30), "Failed: v4_dot_product");
} }
void test_hash_table() {
// Initialize a hash table with key type 'string' and value type 'int'
Hash_Table table = make_hash_table(string, int, get_heap_allocator());
// Test hash_table_set for adding new key-value pairs
string key1 = STR("Key string");
int value1 = 69;
bool newly_added = hash_table_set(&table, key1, value1);
assert(newly_added == true, "Failed: Key should be newly added");
// Test hash_table_find for existing key
int* found_value = hash_table_find(&table, key1);
assert(found_value != NULL, "Failed: Key should exist in hash table");
assert(*found_value == 69, "Failed: Value should be 69, got %i", *found_value);
// Test hash_table_set for updating existing key
int new_value1 = 70;
newly_added = hash_table_set(&table, key1, new_value1);
assert(newly_added == false, "Failed: Key should not be newly added");
// Test hash_table_find for updated value
found_value = hash_table_find(&table, key1);
assert(found_value != NULL, "Failed: Key should exist in hash table");
assert(*found_value == 70, "Failed: Value should be 70, got %i", *found_value);
// Test hash_table_contains for existing key
bool contains = hash_table_contains(&table, key1);
assert(contains == true, "Failed: Hash table should contain key1");
// Test hash_table_contains for non-existing key
string key2 = STR("Non-existing key");
contains = hash_table_contains(&table, key2);
assert(contains == false, "Failed: Hash table should not contain key2");
// Test hash_table_reset
hash_table_reset(&table);
found_value = hash_table_find(&table, key1);
assert(found_value == NULL, "Failed: Hash table should be empty after reset");
// Test hash_table_destroy
hash_table_destroy(&table);
assert(table.entries == NULL, "Failed: Hash table entries should be NULL after destroy");
assert(table.count == 0, "Failed: Hash table count should be 0 after destroy");
assert(table.capacity_count == 0, "Failed: Hash table capacity count should be 0 after destroy");
}
void oogabooga_run_tests() { void oogabooga_run_tests() {
@ -1033,17 +1080,21 @@ void oogabooga_run_tests() {
test_strings(); test_strings();
print("OK!\n"); print("OK!\n");
//print("Thread bombing allocator... "); // #Temporary
//Thread* threads[100]; // This makes entire os freeze in release lol
//for (int i = 0; i < 100; i++) { #if CONFIGURATION != RELEASE
// threads[i] = os_make_thread(test_allocator_threaded, get_heap_allocator()); print("Thread bombing allocator... ");
// os_start_thread(threads[i]); Thread* threads[100];
//} for (int i = 0; i < 100; i++) {
//for (int i = 0; i < 100; i++) { threads[i] = os_make_thread(test_allocator_threaded, get_heap_allocator());
// os_join_thread(threads[i]); os_start_thread(threads[i]);
//} }
//print("OK!\n"); for (int i = 0; i < 100; i++) {
os_join_thread(threads[i]);
}
print("OK!\n");
#endif
print("Testing file IO... "); print("Testing file IO... ");
test_file_io(); test_file_io();
@ -1057,4 +1108,8 @@ void oogabooga_run_tests() {
test_simd(); test_simd();
print("OK!\n"); print("OK!\n");
print("Testing hash table... ");
test_hash_table();
print("OK!\n");
} }