Skip to content

Commit

Permalink
Hash map
Browse files Browse the repository at this point in the history
  • Loading branch information
Novum committed Jan 16, 2023
1 parent bec08af commit 8d17d2e
Show file tree
Hide file tree
Showing 9 changed files with 450 additions and 0 deletions.
1 change: 1 addition & 0 deletions Quake/common.make
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ OBJS := strlcat.o \
world.o \
mem.o \
tasks.o \
hash_map.o \
embedded_pak.o \
$(SYSOBJ_SYS) $(SYSOBJ_MAIN)

Expand Down
358 changes: 358 additions & 0 deletions Quake/hash_map.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
/*
Copyright (C) 2023 Axel Gneiting
*/

#include "quakedef.h"

#define LOAD_FACTOR 0.75f
#define MIN_KEY_VALUE_STORAGE_SIZE 16
#define MIN_HASH_SIZE 32

typedef struct hash_map_s
{
uint32_t num_entries;
uint32_t hash_size;
uint32_t key_value_storage_size;
uint32_t key_size;
uint32_t value_size;
uint32_t (*hasher) (const void *const);
uint32_t *hash_to_index;
uint32_t *index_chain;
void *keys;
void *values;
} hash_map_t;

/*
=================
HashMap_GetKeyImpl
=================
*/
void *HashMap_GetKeyImpl (hash_map_t *map, uint32_t index)
{
return (byte *)map->keys + (map->key_size * index);
}

/*
=================
HashMap_GetValueImpl
=================
*/
void *HashMap_GetValueImpl (hash_map_t *map, uint32_t index)
{
return (byte *)map->values + (map->value_size * index);
}

/*
=================
HashMap_Rehash
=================
*/
static void HashMap_Rehash (hash_map_t *map, const uint32_t new_size)
{
if (map->hash_size >= new_size)
return;
map->hash_size = new_size;
map->hash_to_index = Mem_Realloc (map->hash_to_index, map->hash_size * sizeof (uint32_t));
memset (map->hash_to_index, 0xFF, map->hash_size * sizeof (uint32_t));
for (uint32_t i = 0; i < map->num_entries; ++i)
{
void *key = HashMap_GetKeyImpl (map, i);
const uint32_t hash = map->hasher (key);
const uint32_t hash_index = hash & (map->hash_size - 1);
map->index_chain[i] = map->hash_to_index[hash_index];
map->hash_to_index[hash_index] = i;
}
}

/*
=================
HashMap_ExpandKeyValueStorage
=================
*/
static void HashMap_ExpandKeyValueStorage (hash_map_t *map, const uint32_t new_size)
{
map->keys = Mem_Realloc (map->keys, new_size * map->key_size);
map->values = Mem_Realloc (map->values, new_size * map->value_size);
map->index_chain = Mem_Realloc (map->index_chain, new_size * sizeof (uint32_t));
map->key_value_storage_size = new_size;
}

/*
=================
HashMap_CreateImpl
=================
*/
hash_map_t *HashMap_CreateImpl (const uint32_t key_size, const uint32_t value_size, uint32_t (*hasher) (const void *const))
{
hash_map_t *map = Mem_Alloc (sizeof (hash_map_t));
map->key_size = key_size;
map->value_size = value_size;
map->hasher = hasher;
return map;
}

/*
=================
HashMap_Destroy
=================
*/
void HashMap_Destroy (hash_map_t *map)
{
Mem_Free (map->hash_to_index);
Mem_Free (map->index_chain);
Mem_Free (map->keys);
Mem_Free (map->values);
Mem_Free (map);
}

/*
=================
HashMap_Reserve
=================
*/
void HashMap_Reserve (hash_map_t *map, int capacity)
{
const uint32_t new_key_value_storage_size = Q_nextPow2 (capacity);
if (map->key_value_storage_size < new_key_value_storage_size)
HashMap_ExpandKeyValueStorage (map, Q_nextPow2 (capacity));
const uint32_t new_hash_size = Q_nextPow2 (ceilf (capacity / LOAD_FACTOR));
if (map->hash_size < new_hash_size)
HashMap_Rehash (map, new_hash_size);
}

/*
=================
HashMap_Insert
=================
*/
qboolean HashMap_Insert (hash_map_t *map, const void *const key, const void *const value)
{
if (map->num_entries >= map->key_value_storage_size)
HashMap_ExpandKeyValueStorage (map, q_max (map->key_value_storage_size * 2, MIN_KEY_VALUE_STORAGE_SIZE));
if (map->num_entries >= (LOAD_FACTOR * map->hash_size))
HashMap_Rehash (map, q_max (map->hash_size * 2, MIN_HASH_SIZE));

const uint32_t hash = map->hasher (key);
const uint32_t hash_index = hash & (map->hash_size - 1);
{
uint32_t storage_index = map->hash_to_index[hash_index];
while (storage_index != UINT32_MAX)
{
if (memcmp (key, HashMap_GetKeyImpl (map, storage_index), map->key_size) == 0)
{
memcpy (HashMap_GetValueImpl (map, storage_index), value, map->value_size);
return true;
}
storage_index = map->index_chain[storage_index];
}
}

map->index_chain[map->num_entries] = map->hash_to_index[hash_index];
map->hash_to_index[hash_index] = map->num_entries;
memcpy (HashMap_GetKeyImpl (map, map->num_entries), key, map->key_size);
memcpy (HashMap_GetValueImpl (map, map->num_entries), value, map->value_size);
++map->num_entries;

return false;
}

/*
=================
HashMap_Erase
=================
*/
qboolean HashMap_Erase (hash_map_t *map, const void *const key)
{
if (map->num_entries == 0)
return false;

const uint32_t hash = map->hasher (key);
const uint32_t hash_index = hash & (map->hash_size - 1);
uint32_t storage_index = map->hash_to_index[hash_index];
uint32_t *prev_storage_index_ptr = NULL;
while (storage_index != UINT32_MAX)
{
if (memcmp (key, HashMap_GetKeyImpl (map, storage_index), map->key_size) == 0)
{
{
// Remove found key from index
if (prev_storage_index_ptr == NULL)
map->hash_to_index[hash_index] = map->index_chain[storage_index];
else
*prev_storage_index_ptr = map->index_chain[storage_index];
}

const uint32_t last_index = map->num_entries - 1;
const uint32_t last_hash = map->hasher (HashMap_GetKeyImpl (map, last_index));
const uint32_t last_hash_index = last_hash & (map->hash_size - 1);

if (storage_index == last_index)
{
--map->num_entries;
return true;
}

{
// Remove last key from index
if (map->hash_to_index[last_hash_index] == last_index)
map->hash_to_index[last_hash_index] = map->index_chain[last_index];
else
{
qboolean found = false;
for (uint32_t last_storage_index = map->hash_to_index[last_hash_index]; last_storage_index != UINT32_MAX;
last_storage_index = map->index_chain[last_storage_index])
{
if (map->index_chain[last_storage_index] == last_index)
{
map->index_chain[last_storage_index] = map->index_chain[last_index];
found = true;
break;
}
}
assert (found);
}
}

{
// Copy last key to current key position and add back to index
memcpy (HashMap_GetKeyImpl (map, storage_index), HashMap_GetKeyImpl (map, last_index), map->key_size);
memcpy (HashMap_GetValueImpl (map, storage_index), HashMap_GetValueImpl (map, last_index), map->value_size);
map->index_chain[storage_index] = map->hash_to_index[last_hash_index];
map->hash_to_index[last_hash_index] = storage_index;
}

--map->num_entries;
return true;
}
prev_storage_index_ptr = &map->index_chain[storage_index];
storage_index = map->index_chain[storage_index];
}
return false;
}

/*
=================
HashMap_LookupImpl
=================
*/
void *HashMap_LookupImpl (hash_map_t *map, const void *const key)
{
if (map->num_entries == 0)
return NULL;

const uint32_t hash = map->hasher (key);
const uint32_t hash_index = hash & (map->hash_size - 1);
uint32_t storage_index = map->hash_to_index[hash_index];
while (storage_index != UINT32_MAX)
{
if (memcmp (key, (byte *)map->keys + (storage_index * map->key_size), map->key_size) == 0)
return (byte *)map->values + (storage_index * map->value_size);
storage_index = map->index_chain[storage_index];
}

return NULL;
}

/*
=================
HashMap_Size
=================
*/
uint32_t HashMap_Size (hash_map_t *map)
{
return map->num_entries;
}

#ifdef _DEBUG
/*
=================
HashMap_TestAssert
=================
*/
#define HashMap_TestAssert(cond, what) \
if (!(cond)) \
{ \
Con_Printf (what); \
abort (); \
}

/*
=================
HashMap_BasicTest
=================
*/
static void HashMap_BasicTest (const qboolean reserve)
{
const int TEST_SIZE = 1000;
hash_map_t *map = HashMap_Create (int32_t, int64_t, &HashInt32);
if (reserve)
HashMap_Reserve (map, TEST_SIZE);
for (int i = 0; i < TEST_SIZE; ++i)
{
int64_t value = i;
HashMap_TestAssert (!HashMap_Insert (map, &i, &value), va ("%d should not be overwritten\n", i));
}
for (int i = 0; i < TEST_SIZE; ++i)
HashMap_TestAssert (*HashMap_Lookup (int64_t, map, &i) == i, va ("Wrong lookup for %d\n", i));
for (int i = 0; i < TEST_SIZE; i += 2)
HashMap_Erase (map, &i);
for (int i = 1; i < TEST_SIZE; i += 2)
HashMap_TestAssert (*HashMap_Lookup (int64_t, map, &i) == i, va ("Wrong lookup for %d\n", i));
for (int i = 0; i < TEST_SIZE; i += 2)
HashMap_TestAssert (HashMap_Lookup (int64_t, map, &i) == NULL, va ("Wrong lookup for %d\n", i));
for (int i = 0; i < TEST_SIZE; ++i)
HashMap_Erase (map, &i);
HashMap_TestAssert (HashMap_Size (map) == 0, "Map is not empty");
for (int i = 0; i < TEST_SIZE; ++i)
HashMap_TestAssert (HashMap_Lookup (int64_t, map, &i) == NULL, va ("Wrong lookup for %d\n", i));
HashMap_Destroy (map);
}

/*
=================
HashMap_BasicTest
=================
*/
static void HashMap_StressTest (void)
{
srand (0);
const int TEST_SIZE = 10000;
TEMP_ALLOC (int64_t, keys, TEST_SIZE);
hash_map_t *map = HashMap_Create (int64_t, int32_t, &HashInt64);
for (int j = 0; j < 10; ++j)
{
for (int i = 0; i < TEST_SIZE; ++i)
{
keys[i] = i;
}
for (int i = TEST_SIZE - 1; i > 0; --i)
{
const int swap_index = rand () % (i + 1);
const int temp = keys[swap_index];
keys[swap_index] = keys[i];
keys[i] = temp;
}
for (int i = 0; i < TEST_SIZE; ++i)
HashMap_Insert (map, &keys[i], &i);
for (int i = 0; i < TEST_SIZE; ++i)
HashMap_TestAssert (*HashMap_Lookup (int32_t, map, &keys[i]) == i, va ("Wrong lookup for %d\n", i));
for (int i = TEST_SIZE - 1; i >= 0; --i)
HashMap_Erase (map, &keys[i]);
HashMap_TestAssert (HashMap_Size (map) == 0, "Map is not empty");
}
HashMap_Destroy (map);
TEMP_FREE (keys);
}

/*
=================
TestHashMap_f
=================
*/
void TestHashMap_f (void)
{
HashMap_BasicTest (false);
HashMap_BasicTest (true);
HashMap_StressTest ();
}
#endif
Loading

0 comments on commit 8d17d2e

Please sign in to comment.