diff --git a/.gitignore b/.gitignore index d83a1b2..4b528dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ benchmark +hm_test diff --git a/Makefile b/Makefile index 68d2394..93c0655 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,18 @@ +CC=cc +CFLAGS=-std=c11 -Wall -Wextra CXX=g++ -CXXFLAGS=-Wall -Wextra -O3 +CXXFLAGS=-std=c++11 -Wall -Wextra -O3 -benchmark: benchmark.cpp +benchmark: benchmark.cpp hm.h $(CXX) $(CXXFLAGS) -o benchmark benchmark.cpp +hm_test: test.c hm.h + $(CC) $(CFLAGS) -o hm_test test.c + +test: hm_test + ./hm_test + clean: - rm -rf benchmark + rm -f benchmark hm_test + +.PHONY: test clean diff --git a/README.md b/README.md index 3768687..011ac5b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,36 @@ # hm.h -A single header library that implements chained hash table in C. +A single header, C11 compliant, library that implements a +[hash table](https://en.wikipedia.org/wiki/Hash_table#Separate_chaining) +in C using separate chaining. + +## Example + +```c +#define HM_IMPLEMENTATION +#include "hm.h" + +#include + +int main(void) +{ + HashMap ages = {0}; // or hm_create(...) to specify custom key and + // value sizes, a size of 0 means a NULL + // terminated string + + hm_put(&ages, "alice", "30"); + hm_put(&ages, "bob", "25"); + + char *alice = hm_get(&ages, "alice"); // NULL if key is not found + printf("alice is %s\n", alice); + + HashMapIterator it = hm_iterate(&ages); + while (hm_next(&it) != NULL) { + printf("%s: %s\n", (const char*)hm_key(&it), (char*)hm_value(&it)); + } + + hm_free(&ages); // The HashMap can still be reused + return 0; +} + +``` diff --git a/benchmark.cpp b/benchmark.cpp index 7577530..97c1083 100644 --- a/benchmark.cpp +++ b/benchmark.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #define HM_IMPLEMENTATION #include "hm.h" @@ -15,16 +16,16 @@ using namespace std::chrono; typedef time_point time_p; // Configuration -const int NUM_ELEMENTS = 1000000; +const size_t NUM_ELEMENTS = 1000000; time_p now() { return high_resolution_clock::now(); } -double time_diff_nano(time_p end, time_p start) +double time_diff_ms(time_p end, time_p start) { - return duration_cast(end - start).count() / 1e6; + return duration(end - start).count(); } void print_div() @@ -49,86 +50,85 @@ void print_row(const std::string& label, double hm_ms, double std_ms) } // Test with string key type -uint64_t hash_str(const void *buffer, size_t size) -{ - (void)size; - const char *str = reinterpret_cast(buffer); - uint64_t hash = (uint64_t)str; - return hash; -} - void test_str() { std::vector key_pool; std::vector value_pool; // Generate random strings to ensure we aren't just hashing "1", "2", "3" - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { key_pool.push_back("key_prefix_" + std::to_string(i) + "_suffix_" + std::to_string(rand() % 1000)); value_pool.push_back("value_" + std::to_string(i)); } // --- Custom implementation --- - // Using 0 for sizes tells your library to treat them as NULL-terminated strings - HashMap hm = hm_create_ex(sizeof(const char *), 0, hash_str); + // Using 0 for sizes tells the library to treat them as NULL-terminated strings + HashMap hm = hm_create(0, 0); // Insertion auto start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { hm_put(&hm, key_pool[i].c_str(), value_pool[i].c_str()); } auto end = now(); - double custom_put = time_diff_nano(end, start); + double custom_put = time_diff_ms(end, start); + + if (hm_count(&hm) != NUM_ELEMENTS) { + std::cerr << "Error in custom insertion!" << std::endl; + return; + } // Lookup start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { - char* val = (char*)hm_get(&hm, key_pool[i].c_str()); - if (!val) { - std::cerr << "Lookup error!" << std::endl; + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { + const char* val = (const char*)hm_get(&hm, key_pool[i].c_str()); + if (!val || strcmp(val, value_pool[i].c_str()) != 0) { + std::cerr << "Error in custom lookup!" << std::endl; return; } } end = now(); - double custom_get = time_diff_nano(end, start); + double custom_get = time_diff_ms(end, start); // Deletion start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { hm_remove(&hm, key_pool[i].c_str()); } end = now(); - double custom_del = time_diff_nano(end, start); + double custom_del = time_diff_ms(end, start); hm_free(&hm); // --- Benchmark std::unordered_map --- - std::unordered_map std_map; + std::unordered_map std_map; // Insertion start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { - std_map[key_pool[i].c_str()] = value_pool[i].c_str(); + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { + std_map[key_pool[i]] = value_pool[i]; } end = now(); - double std_put = time_diff_nano(end, start); + double std_put = time_diff_ms(end, start); // Lookup start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { - auto it = std_map.find(key_pool[i].c_str()); - if (it == std_map.end()) std::cerr << "Lookup error!" << std::endl; + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { + auto it = std_map.find(key_pool[i]); + if (it == std_map.end() || it->second != value_pool[i]) { + std::cerr << "Error in std lookup!" << std::endl; + } } end = now(); - double std_get = time_diff_nano(end, start); + double std_get = time_diff_ms(end, start); // Deletion start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { - std_map.erase(key_pool[i].c_str()); + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { + std_map.erase(key_pool[i]); } end = now(); - double std_del = time_diff_nano(end, start); + double std_del = time_diff_ms(end, start); // Output Results print_head(); @@ -143,7 +143,7 @@ void test_str() uint64_t hash_int(const void *buffer, size_t size) { (void)size; - return *reinterpret_cast(buffer); + return *reinterpret_cast(buffer); } void test_int() @@ -152,9 +152,9 @@ void test_int() std::vector keys(NUM_ELEMENTS); std::vector values(NUM_ELEMENTS); - for (int i = 0; i < NUM_ELEMENTS; ++i) { - keys[i] = i; - values[i] = i * 2; + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { + keys[i] = static_cast(i); + values[i] = static_cast(i * 2); } // --- Custom implementation --- @@ -162,15 +162,15 @@ void test_int() // Insertion auto start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { hm_put(&hm, &keys[i], &values[i]); } auto end = now(); - double custom_put = time_diff_nano(end, start); + double custom_put = time_diff_ms(end, start); // Lookup start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { int* val = (int*)hm_get(&hm, &keys[i]); if (!val || *val != values[i]) { std::cerr << "Error in custom lookup!" << std::endl; @@ -178,15 +178,15 @@ void test_int() } } end = now(); - double custom_get = time_diff_nano(end, start); + double custom_get = time_diff_ms(end, start); // Deletion start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { hm_remove(&hm, &keys[i]); } end = now(); - double custom_del = time_diff_nano(end, start); + double custom_del = time_diff_ms(end, start); hm_free(&hm); @@ -195,30 +195,30 @@ void test_int() // Insertion start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { std_map[keys[i]] = values[i]; } end = now(); - double std_put = time_diff_nano(end, start); + double std_put = time_diff_ms(end, start); // Lookup start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { auto it = std_map.find(keys[i]); if (it == std_map.end() || it->second != values[i]) { std::cerr << "Error in std lookup!" << std::endl; } } end = now(); - double std_get = time_diff_nano(end, start); + double std_get = time_diff_ms(end, start); // Deletion start = now(); - for (int i = 0; i < NUM_ELEMENTS; ++i) { + for (size_t i = 0; i < NUM_ELEMENTS; ++i) { std_map.erase(keys[i]); } end = now(); - double std_del = time_diff_nano(end, start); + double std_del = time_diff_ms(end, start); // Output Results print_head(); diff --git a/hm.h b/hm.h index fe29173..01f8348 100644 --- a/hm.h +++ b/hm.h @@ -1,10 +1,54 @@ -// hm.h - v1.2.0 - MIT License +// hm.h - v1.3.0 - MIT License // chained hash table implementation as a single header library. // // [License and changelog] // // See end of file. // +// [About this library] +// +// This is a header only, C11 compliant, library that implements a hash table +// with separate chaining. +// +// In this library, every slot of a HashMap holds a doubly linked list of +// buckets. Each bucket stores its key and value in a single block of memory, +// so keys and values are copied into the HashMap. They can either be fixed +// size buffers or NULL terminated strings. When the HashMap exceeds its +// maximum load factor, it grows and every bucket is moved to a new slot +// without copying the stored data again. +// +// This library also exposes an iterator to walk through every entry of a +// HashMap, and compile-time options to customize the hash function, +// allocation and growth behavior. +// +// [Quick start example] +// +// #define HM_IMPLEMENTATION +// #include "hm.h" +// +// #include +// +// int main(void) +// { +// HashMap ages = {0}; // or hm_create(...) to specify custom key and +// // value sizes, a size of 0 means a NULL +// // terminated string +// +// hm_put(&ages, "alice", "30"); +// hm_put(&ages, "bob", "25"); +// +// char *alice = hm_get(&ages, "alice"); // NULL if key is not found +// printf("alice is %s\n", alice); +// +// HashMapIterator it = hm_iterate(&ages); +// while (hm_next(&it) != NULL) { +// printf("%s: %s\n", (const char*)hm_key(&it), (char*)hm_value(&it)); +// } +// +// hm_free(&ages); // The HashMap can still be reused +// return 0; +// } +// // [Single header library usage] // // Including this header file as such does what you normally expect. @@ -24,12 +68,12 @@ // #define HM_INITIAL_CAPACITY new_initial_capacity (256) // // This macro defines the initial amount of buckets a HashMap will -// have when allocated. +// have when allocated. It must be a power of two. // // #define HM_DEFAULT_HASH_FUNCTION new_hash_function (hm__fnv1a) // // This macro defines the hash function that will be used by default -// by every new HashMap except the ones (created with 'hm_create_ex'). +// by every new HashMap except the ones created with 'hm_create_ex'. // The specified function has to respect the HmHashFunc type // definition signature. // @@ -41,7 +85,16 @@ // #define HM_GROWTH_FACTOR new_growth_factor (2) // // This macro defines by how much should a HashMap be reallocated when -// rehashing. +// rehashing. It must be a power of two greater than 1. +// +// #define HM_MAX_ALIGNMENT new_max_alignment (alignof(max_align_t)) +// +// This macro defines the maximum alignment that keys and values +// stored in a HashMap are guaranteed to have. A fixed-size key or +// value is aligned to the largest power of two dividing its size, up +// to this limit. It must be a power of two and HM_REALLOC must return +// memory aligned to at least this value. The default is the alignment +// that realloc() guarantees. // // #define HM_NO_ASSERT // @@ -49,6 +102,11 @@ // fail. The disabled assertions will be replaced with a simple // condition that returns false or NULL when they fail. // +// #define HM_ASSERT my_assert (assert) +// +// This macro defines an alternative assertion function. It is ignored +// when HM_NO_ASSERT is defined. +// // #define HM_REALLOC my_realloc (realloc) // #define HM_FREE my_free (free) // @@ -71,23 +129,31 @@ // This functions initializes a HashMap structure with the specified key and // value sizes. If a specified size is 0 then that parameter will be treated // as a NULL terminated buffer for that HashMap. Allocation of the internal -// buckets is done only at the first hm_put() call. +// buckets is done only at the first hm_put() call. A zero-initialized +// HashMap (HashMap hm = {0};) is equivalent to hm_create(0, 0). +// +// Note: fixed-size keys are hashed and compared byte by byte, so struct keys +// with padding must be zero-initialized (e.g. with memset) before +// being filled, otherwise equal keys may not be found. // // HashMap hm_create_ex(size_t key_size, size_t value_size, HmHashFunc hash_func) // // This function is the same as hm_create() but with additional parameters -// for the configuration of the HashMap. +// for the configuration of the HashMap. The hash function receives the key +// and its size in bytes. For NULL terminated keys the size excludes the +// terminator (it equals strlen(key)). // // void hm_free(HashMap *hm) // // This functions frees and invalidates all the keys and values stored in the -// specified HashMap. +// specified HashMap. The HashMap keeps its configuration (key size, value +// size and hash function) and can be reused afterwards. // // bool hm_put(HashMap *hm, const void *key, const void *value) // // This function inserts a new value associated with the specified key. If the // key already exists, it's value will be overridden. Returns false on error -// only if HM_NO_ASSERT is defined. +// only if assertions are disabled (HM_NO_ASSERT is defined). // // WARNING: Passing a 'value' pointer that references memory currently stored // within the HashMap (self-referencing update) results in undefined @@ -96,7 +162,8 @@ // void *hm_get(const HashMap *hm, const void *key) // // This function returns the value associated with the specified key. Returns -// NULL if key was not found. +// NULL if key was not found. The returned pointer is suitably aligned for a +// value of the HashMap's fixed value size (see HM_MAX_ALIGNMENT). // // type *hm_of(type, hm, key) // @@ -119,6 +186,10 @@ // HashMap. Note that the returned iterator does not start from the first // element of the HashMap. You'll need to call hm_next() at least once. // +// WARNING: Calling hm_put(), hm_remove() or hm_free() on a HashMap invalidates +// all of its iterators. To remove entries while iterating, collect +// their keys first and remove them after the iteration. +// // const void *hm_key(const HashMapIterator *it) // // This function returns the key of the current entry. Returns NULL if the @@ -141,6 +212,7 @@ #ifndef HM_H_ #define HM_H_ +#include #include #include #include @@ -162,12 +234,17 @@ # define HM_GROWTH_FACTOR 2 #endif // HM_GROWTH_FACTOR +#ifndef HM_MAX_ALIGNMENT +# define HM_MAX_ALIGNMENT alignof(max_align_t) +#endif // HM_MAX_ALIGNMENT + #ifndef HM_NO_ASSERT # ifndef HM_ASSERT # include # define HM_ASSERT assert # endif // HM_ASSERT #else +# undef HM_ASSERT # define HM_ASSERT(...) ((void)0) #endif // HM_NO_ASSERT @@ -181,11 +258,8 @@ # define HM_FREE free #endif // HM_FREE -#ifdef __cplusplus -extern "C" { // Prevent name mangling -#endif // __cplusplus - -// Type definition for a hash function returning a 64-bit hash +// Type definition for a hash function returning a 64-bit hash. For NULL +// terminated keys 'size' does not include the terminator. typedef uint64_t (*HmHashFunc)(const void *buffer, size_t size); typedef struct Hm__Bucket { @@ -194,6 +268,7 @@ typedef struct Hm__Bucket { void *key; void *value; size_t value_size; + uint64_t hash; } Hm__Bucket; typedef struct { @@ -212,6 +287,10 @@ typedef struct { bool end; } HashMapIterator; +#ifdef __cplusplus +extern "C" { // Prevent name mangling +#endif // __cplusplus + // Create and free HashMap hm_create(size_t key_size, size_t value_size); HashMap hm_create_ex(size_t key_size, size_t value_size, HmHashFunc hash_func); @@ -220,7 +299,7 @@ void hm_free(HashMap *hm); // Modify, access and remove bool hm_put(HashMap *hm, const void *key, const void *value); void *hm_get(const HashMap *hm, const void *key); -#define hm_of(type, hm, key) (type*)hm_get((hm), (key)) +#define hm_of(type, hm, key) ((type*)hm_get((hm), (key))) bool hm_remove(HashMap *hm, const void *key); size_t hm_count(const HashMap *hm); @@ -230,14 +309,6 @@ const void *hm_key(const HashMapIterator *it); void *hm_value(const HashMapIterator *it); void *hm_next(HashMapIterator *it); -// Private functions -uint64_t hm__fnv1a(const void *buffer, size_t size); -size_t hm__key_size(const HashMap *hm, const void *key); -size_t hm__value_size(const HashMap *hm, const void *value); -bool hm__keycmp(const HashMap *hm, const void *key1, const void *key2); -bool hm__rehash(HashMap *hm); -Hm__Bucket *hm__bucket_create(const void *key, size_t key_size, const void *value, size_t value_size); - #ifdef __cplusplus } #endif // __cplusplus @@ -246,9 +317,16 @@ Hm__Bucket *hm__bucket_create(const void *key, size_t key_size, const void *valu #ifdef HM_IMPLEMENTATION -#ifdef __cplusplus -extern "C" { // Prevent name mangling -#endif // __cplusplus +// Private functions +uint64_t hm__fnv1a(const void *buffer, size_t size); +uint64_t hm__hash(const HashMap *hm, const void *key, size_t key_size); +size_t hm__key_size(const HashMap *hm, const void *key); +size_t hm__value_size(const HashMap *hm, const void *value); +bool hm__keycmp(const HashMap *hm, const void *key1, const void *key2); +bool hm__rehash(HashMap *hm); +size_t hm__alignment(size_t size); +size_t hm__bucket_layout(const HashMap *hm, size_t key_size, size_t value_size, size_t *key_offset, size_t *value_offset); +Hm__Bucket *hm__bucket_create(const HashMap *hm, const void *key, size_t key_size, const void *value, size_t value_size, uint64_t hash); HashMap hm_create(size_t key_size, size_t value_size) { @@ -280,26 +358,23 @@ void hm_free(HashMap *hm) return; } - if (hm->map == NULL) { - memset(hm, 0, sizeof(*hm)); - return; - } - - for (size_t i = 0; i < hm->capacity; ++i) { - Hm__Bucket *cur = hm->map[i]; - if (cur == NULL) { - continue; + if (hm->map != NULL) { + for (size_t i = 0; i < hm->capacity; ++i) { + Hm__Bucket *cur = hm->map[i]; + while (cur != NULL) { + Hm__Bucket *next = cur->next; + HM_FREE(cur); + cur = next; + } } - while (cur != NULL) { - Hm__Bucket *next = cur->next; - HM_FREE(cur); - cur = next; - } + HM_FREE(hm->map); } - HM_FREE(hm->map); - memset(hm, 0, sizeof(*hm)); + // Keep the configuration so that the HashMap can be reused + hm->map = NULL; + hm->count = 0; + hm->capacity = HM_INITIAL_CAPACITY; } bool hm_put(HashMap *hm, const void *key, const void *value) @@ -320,24 +395,15 @@ bool hm_put(HashMap *hm, const void *key, const void *value) memset(hm->map, 0, sizeof(*hm->map) * hm->capacity); } - // Rehash if necessary - if ((float)hm->count + 1 > (float)hm->capacity * HM_MAX_LOAD_FACTOR) { - if (!hm__rehash(hm)) { - return false; - } - } - // Key-value sizes size_t key_size = hm__key_size(hm, key); size_t value_size = hm__value_size(hm, value); - - // Get the head bucket - size_t idx = hm->hash_func(key, key_size) % hm->capacity; - Hm__Bucket *head = hm->map[idx]; + uint64_t hash = hm__hash(hm, key, key_size); // Check if key already exists - for (Hm__Bucket *cur = head; cur != NULL; cur = cur->next) { - if (!hm__keycmp(hm, key, cur->key)) { + Hm__Bucket **slot = &hm->map[(size_t)hash & (hm->capacity - 1)]; + for (Hm__Bucket *cur = *slot; cur != NULL; cur = cur->next) { + if (cur->hash != hash || !hm__keycmp(hm, key, cur->key)) { continue; } @@ -348,7 +414,8 @@ bool hm_put(HashMap *hm, const void *key, const void *value) } // Reallocate the entire bucket - size_t size = sizeof(Hm__Bucket) + key_size + value_size; + size_t key_offset, value_offset; + size_t size = hm__bucket_layout(hm, key_size, value_size, &key_offset, &value_offset); Hm__Bucket *new_bucket = (Hm__Bucket*)HM_REALLOC(cur, size); if (new_bucket == NULL) { HM_ASSERT(false && "Reallocation failed"); @@ -356,8 +423,8 @@ bool hm_put(HashMap *hm, const void *key, const void *value) } // Recalculate key-value pointers - new_bucket->key = (uint8_t*)new_bucket + sizeof(*new_bucket); - new_bucket->value = (uint8_t*)new_bucket->key + key_size; + new_bucket->key = (uint8_t*)new_bucket + key_offset; + new_bucket->value = (uint8_t*)new_bucket + value_offset; new_bucket->value_size = value_size; // Update linked list pointers of neighbors @@ -365,8 +432,11 @@ bool hm_put(HashMap *hm, const void *key, const void *value) if (new_bucket->prev != NULL) { new_bucket->prev->next = new_bucket; } else { - size_t idx = hm->hash_func(key, key_size) % hm->capacity; - hm->map[idx] = new_bucket; + *slot = new_bucket; + } + + if (new_bucket->next != NULL) { + new_bucket->next->prev = new_bucket; } } @@ -376,12 +446,22 @@ bool hm_put(HashMap *hm, const void *key, const void *value) return true; } + // Rehash if necessary, only when a new entry is going to be inserted + if ((double)(hm->count + 1) > (double)hm->capacity * (double)HM_MAX_LOAD_FACTOR) { + if (!hm__rehash(hm)) { + return false; + } + } + // Insert the new value - Hm__Bucket *new_bucket = hm__bucket_create(key, key_size, value, value_size); + Hm__Bucket *new_bucket = hm__bucket_create(hm, key, key_size, value, value_size, hash); if (new_bucket == NULL) { return false; } + size_t idx = (size_t)hash & (hm->capacity - 1); + Hm__Bucket *head = hm->map[idx]; + new_bucket->next = head; if (head != NULL) { head->prev = new_bucket; @@ -404,15 +484,11 @@ void *hm_get(const HashMap *hm, const void *key) } size_t key_size = hm__key_size(hm, key); - size_t idx = hm->hash_func(key, key_size) % hm->capacity; + uint64_t hash = hm__hash(hm, key, key_size); + size_t idx = (size_t)hash & (hm->capacity - 1); - Hm__Bucket *head = hm->map[idx]; - if (head == NULL) { - return NULL; - } - - for (Hm__Bucket *cur = head; cur != NULL; cur = cur->next) { - if (hm__keycmp(hm, key, cur->key)) { + for (Hm__Bucket *cur = hm->map[idx]; cur != NULL; cur = cur->next) { + if (cur->hash == hash && hm__keycmp(hm, key, cur->key)) { return cur->value; } } @@ -427,19 +503,15 @@ bool hm_remove(HashMap *hm, const void *key) } size_t key_size = hm__key_size(hm, key); - size_t idx = hm->hash_func(key, key_size) % hm->capacity; + uint64_t hash = hm__hash(hm, key, key_size); + size_t idx = (size_t)hash & (hm->capacity - 1); - Hm__Bucket *head = hm->map[idx]; - if (head == NULL) { - return false; - } - - for (Hm__Bucket *cur = head; cur != NULL; cur = cur->next) { - if (!hm__keycmp(hm, key, cur->key)) { + for (Hm__Bucket *cur = hm->map[idx]; cur != NULL; cur = cur->next) { + if (cur->hash != hash || !hm__keycmp(hm, key, cur->key)) { continue; } - if (cur == head) { + if (cur->prev == NULL) { hm->map[idx] = cur->next; } else { cur->prev->next = cur->next; @@ -532,27 +604,14 @@ void *hm_next(HashMapIterator *it) return NULL; } - // Find first list of buckets when iterator hasn't started yet - if (it->bucket == NULL) { - while (it->index < hm->capacity) { - it->bucket = hm->map[it->index++]; - if (it->bucket != NULL) { - return it->bucket->value; - } - } - it->end = true; - it->bucket = NULL; - it->index = hm->capacity; - return NULL; - } - // Advance within current chain - if (it->bucket->next != NULL) { + if (it->bucket != NULL && it->bucket->next != NULL) { it->bucket = it->bucket->next; return it->bucket->value; } - // Move to next non-empty bucket array slot + // Move to next non-empty bucket array slot (or the first one when the + // iterator hasn't started yet) while (it->index < hm->capacity) { it->bucket = hm->map[it->index++]; if (it->bucket != NULL) { @@ -573,8 +632,6 @@ uint64_t hm__fnv1a(const void *buffer, size_t size) return 0; } - // TODO: When buffer is NULL terminated, the NULL is included in the hash - const uint8_t *b = (const uint8_t*)buffer; uint64_t hash = 14695981039346656037ULL; @@ -586,6 +643,21 @@ uint64_t hm__fnv1a(const void *buffer, size_t size) return hash; } +uint64_t hm__hash(const HashMap *hm, const void *key, size_t key_size) +{ + // NULL terminated keys are hashed without their terminator + if (hm->key_size == 0) { + key_size -= 1; + } + + // A zero-initialized HashMap has no hash function set + if (hm->hash_func == NULL) { + return HM_DEFAULT_HASH_FUNCTION(key, key_size); + } + + return hm->hash_func(key, key_size); +} + size_t hm__key_size(const HashMap *hm, const void *key) { if (hm == NULL || key == NULL) { @@ -641,9 +713,7 @@ bool hm__rehash(HashMap *hm) Hm__Bucket *cur = hm->map[i]; while (cur != NULL) { Hm__Bucket *next = cur->next; - - size_t key_size = hm__key_size(hm, cur->key); - uint64_t idx = hm->hash_func(cur->key, key_size) % new_capacity; + size_t idx = (size_t)cur->hash & (new_capacity - 1); // Insert new bucket cur->prev = NULL; @@ -664,16 +734,41 @@ bool hm__rehash(HashMap *hm) return true; } -Hm__Bucket *hm__bucket_create(const void *key, size_t key_size, const void *value, size_t value_size) +size_t hm__alignment(size_t size) { - if (key == NULL || value == NULL) { + // NULL terminated buffers only need byte alignment + if (size == 0) { + return 1; + } + + // A type's alignment always divides its size, so the largest power of + // two dividing the size is a safe alignment for it + size_t alignment = size & (~size + 1); + return (alignment < HM_MAX_ALIGNMENT ? alignment : HM_MAX_ALIGNMENT); +} + +size_t hm__bucket_layout(const HashMap *hm, size_t key_size, size_t value_size, size_t *key_offset, size_t *value_offset) +{ + // [ Hm__Bucket ][ padding ][ key ][ padding ][ value ] + size_t key_align = hm__alignment(hm->key_size); + size_t value_align = hm__alignment(hm->value_size); + + *key_offset = (sizeof(Hm__Bucket) + key_align - 1) & ~(key_align - 1); + *value_offset = (*key_offset + key_size + value_align - 1) & ~(value_align - 1); + + return *value_offset + value_size; +} + +Hm__Bucket *hm__bucket_create(const HashMap *hm, const void *key, size_t key_size, const void *value, size_t value_size, uint64_t hash) +{ + if (hm == NULL || key == NULL || value == NULL) { HM_ASSERT(false && "Invalid parameters"); return NULL; } // Allocate bucket - // [ Hm__Bucket ][ key ][ value ] - size_t size = sizeof(Hm__Bucket) + key_size + value_size; + size_t key_offset, value_offset; + size_t size = hm__bucket_layout(hm, key_size, value_size, &key_offset, &value_offset); Hm__Bucket *bucket = (Hm__Bucket*)HM_REALLOC(NULL, size); if (bucket == NULL) { HM_ASSERT(false && "Reallocation failed"); @@ -682,9 +777,10 @@ Hm__Bucket *hm__bucket_create(const void *key, size_t key_size, const void *valu memset(bucket, 0, sizeof(*bucket)); // Prepare key-value pointers offset - bucket->key = (uint8_t*)bucket + sizeof(*bucket); - bucket->value = (uint8_t*)bucket->key + key_size; + bucket->key = (uint8_t*)bucket + key_offset; + bucket->value = (uint8_t*)bucket + value_offset; bucket->value_size = value_size; + bucket->hash = hash; // Copy key-value data memcpy(bucket->key, key, key_size); @@ -693,15 +789,30 @@ Hm__Bucket *hm__bucket_create(const void *key, size_t key_size, const void *valu return bucket; } -#ifdef __cplusplus -} -#endif // __cplusplus - #endif // HM_IMPLEMENTATION /* * Revision history: * + * 1.3.0 (2026-10-28) Fix use-after-free when updating a value whose size + * changed in a bucket chain; + * Fix misaligned keys and values, new HM_MAX_ALIGNMENT + * compile-time option; + * Fix crash when using a zero-initialized HashMap; + * hm_free() now keeps the HashMap configuration so it + * can be reused; + * Updating an existing key no longer triggers a rehash; + * Buckets store their hash for faster lookups and + * rehashing; + * HM_INITIAL_CAPACITY and HM_GROWTH_FACTOR must now be + * powers of two; + * Fix hm_of() macro precedence; + * The library now targets C11 (or C++11) and + * HM_MAX_ALIGNMENT defaults to alignof(max_align_t); + * NULL terminated keys are hashed without their + * terminator, hash functions now receive strlen(key) + * as size for them; + * Improved documentation * 1.2.0 (2026-04-08) New HmHashFunc type, configurable by the user with * hm_create_ex() or HM_DEFAULT_HASH_FUNCTION * 1.1.2 (2026-04-08) Optimized Hm__Bucket creation by only allocating one diff --git a/test.c b/test.c new file mode 100644 index 0000000..0b10360 --- /dev/null +++ b/test.c @@ -0,0 +1,268 @@ +#define HM_IMPLEMENTATION +#include "hm.h" + +#include + +static int failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "%s:%d: CHECK failed: %s\n", \ + __FILE__, __LINE__, #cond); \ + ++failures; \ + } \ + } while (0) + +// Forces every key into the same chain +static uint64_t const_hash(const void *buffer, size_t size) +{ + (void)buffer; + (void)size; + return 0; +} + +static void test_int_keys(void) +{ + HashMap hm = hm_create(sizeof(int), sizeof(int)); + const int n = 10000; + + for (int i = 0; i < n; ++i) { + int v = i * 2; + CHECK(hm_put(&hm, &i, &v)); + } + CHECK(hm_count(&hm) == (size_t)n); + + for (int i = 0; i < n; ++i) { + int *v = hm_of(int, &hm, &i); + CHECK(v != NULL && *v == i * 2); + } + + int missing = n; + CHECK(hm_get(&hm, &missing) == NULL); + + for (int i = 0; i < n; i += 2) { + CHECK(hm_remove(&hm, &i)); + } + CHECK(hm_count(&hm) == (size_t)n / 2); + for (int i = 0; i < n; ++i) { + CHECK((hm_get(&hm, &i) != NULL) == (i % 2 == 1)); + } + + hm_free(&hm); +} + +static void test_string_value_resize_in_chain(void) +{ + HashMap hm = hm_create_ex(0, 0, const_hash); + char big[4096]; + memset(big, 'y', sizeof(big) - 1); + big[sizeof(big) - 1] = '\0'; + + // Chain: c -> b -> a + CHECK(hm_put(&hm, "a", "x")); + CHECK(hm_put(&hm, "b", "x")); + CHECK(hm_put(&hm, "c", "x")); + + // Grow every bucket so realloc moves them, then shrink them back + CHECK(hm_put(&hm, "a", big)); + CHECK(hm_put(&hm, "b", big)); + CHECK(hm_put(&hm, "c", big)); + CHECK(strcmp((char*)hm_get(&hm, "b"), big) == 0); + CHECK(hm_put(&hm, "b", "short")); + CHECK(strcmp((char*)hm_get(&hm, "b"), "short") == 0); + CHECK(hm_count(&hm) == 3); + + // Removing neighbors walks the prev/next pointers fixed up above + CHECK(hm_remove(&hm, "a")); + CHECK(hm_remove(&hm, "c")); + CHECK(hm_remove(&hm, "b")); + CHECK(hm_count(&hm) == 0); + + hm_free(&hm); +} + +static void test_alignment(void) +{ + // Variable length string keys followed by doubles + HashMap hm = hm_create(0, sizeof(double)); + const char *keys[] = { "a", "ab", "abc", "abcd", "abcde", "abcdefg" }; + for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++i) { + double d = (double)i + 0.5; + CHECK(hm_put(&hm, keys[i], &d)); + double *p = hm_of(double, &hm, keys[i]); + CHECK(p != NULL && (uintptr_t)p % alignof(double) == 0); + CHECK(p != NULL && *p == d); + } + hm_free(&hm); + + // Odd-sized struct keys followed by struct values + typedef struct { int a, b, c; } Key; + typedef struct { double x; long long y; } Value; + hm = hm_create(sizeof(Key), sizeof(Value)); + for (int i = 0; i < 100; ++i) { + Key k; + memset(&k, 0, sizeof(k)); + k.a = i; + Value v = { (double)i, i }; + CHECK(hm_put(&hm, &k, &v)); + CHECK((uintptr_t)hm_get(&hm, &k) % alignof(Value) == 0); + CHECK((uintptr_t)hm_get(&hm, &k) % alignof(double) == 0); + CHECK(hm_of(Value, &hm, &k)->y == i); + } + HashMapIterator it = hm_iterate(&hm); + while (hm_next(&it) != NULL) { + CHECK((uintptr_t)hm_key(&it) % alignof(int) == 0); + } + hm_free(&hm); +} + +static void test_zero_initialized(void) +{ + HashMap hm = {0}; + + CHECK(hm_get(&hm, "key") == NULL); + CHECK(!hm_remove(&hm, "key")); + CHECK(hm_put(&hm, "key", "value")); + CHECK(strcmp((char*)hm_get(&hm, "key"), "value") == 0); + + hm_free(&hm); +} + +static void test_reuse_after_free(void) +{ + HashMap hm = hm_create(sizeof(int), sizeof(int)); + int k = 1, v = 2; + + CHECK(hm_put(&hm, &k, &v)); + hm_free(&hm); + CHECK(hm_count(&hm) == 0); + CHECK(hm_get(&hm, &k) == NULL); + + // Configuration survives hm_free() + v = 3; + CHECK(hm_put(&hm, &k, &v)); + CHECK(*hm_of(int, &hm, &k) == 3); + + hm_free(&hm); + hm_free(&hm); +} + +static void test_iteration(void) +{ + HashMap hm = hm_create(sizeof(int), sizeof(int)); + const int n = 1000; + static bool seen[1000]; + + HashMapIterator it = hm_iterate(&hm); + CHECK(hm_next(&it) == NULL); + + for (int i = 0; i < n; ++i) { + CHECK(hm_put(&hm, &i, &i)); + } + + size_t visited = 0; + it = hm_iterate(&hm); + while (hm_next(&it) != NULL) { + int key = *(const int*)hm_key(&it); + CHECK(key >= 0 && key < n && !seen[key]); + CHECK(*(int*)hm_value(&it) == key); + seen[key] = true; + ++visited; + } + CHECK(visited == (size_t)n); + CHECK(hm_key(&it) == NULL && hm_value(&it) == NULL); + + hm_free(&hm); +} + +static void test_update_does_not_rehash(void) +{ + HashMap hm = hm_create(sizeof(int), sizeof(int)); + const int n = (int)(HM_INITIAL_CAPACITY * HM_MAX_LOAD_FACTOR); + + for (int i = 0; i < n; ++i) { + CHECK(hm_put(&hm, &i, &i)); + } + CHECK(hm.capacity == HM_INITIAL_CAPACITY); + + int k = 0, v = 42; + CHECK(hm_put(&hm, &k, &v)); + CHECK(hm.capacity == HM_INITIAL_CAPACITY); + CHECK(*hm_of(int, &hm, &k) == 42); + + hm_free(&hm); +} + +static void test_custom_hash(void) +{ + HashMap hm = hm_create_ex(0, sizeof(int), const_hash); + char key[16]; + + for (int i = 0; i < 500; ++i) { + snprintf(key, sizeof(key), "k%d", i); + CHECK(hm_put(&hm, key, &i)); + } + for (int i = 0; i < 500; ++i) { + snprintf(key, sizeof(key), "k%d", i); + CHECK(*hm_of(int, &hm, key) == i); + } + hm_free(&hm); + + hm = hm_create_ex(0, 0, NULL); + CHECK(hm.hash_func == HM_DEFAULT_HASH_FUNCTION); + hm_free(&hm); +} + +static size_t last_hash_size; + +// Records the size it receives +static uint64_t size_hash(const void *buffer, size_t size) +{ + last_hash_size = size; + return hm__fnv1a(buffer, size); +} + +static void test_hash_size(void) +{ + // NULL terminated keys are hashed without their terminator + HashMap hm = hm_create_ex(0, 0, size_hash); + CHECK(hm_put(&hm, "abc", "v")); + CHECK(last_hash_size == 3); + CHECK(hm_get(&hm, "abc") != NULL); + CHECK(last_hash_size == 3); + CHECK(hm_put(&hm, "", "empty")); + CHECK(last_hash_size == 0); + CHECK(strcmp((char*)hm_get(&hm, ""), "empty") == 0); + CHECK(hm_remove(&hm, "")); + CHECK(last_hash_size == 0); + hm_free(&hm); + + // Fixed-size keys are hashed with their full size + hm = hm_create_ex(sizeof(int), sizeof(int), size_hash); + int k = 7; + CHECK(hm_put(&hm, &k, &k)); + CHECK(last_hash_size == sizeof(int)); + hm_free(&hm); +} + +int main(void) +{ + test_int_keys(); + test_string_value_resize_in_chain(); + test_alignment(); + test_zero_initialized(); + test_reuse_after_free(); + test_iteration(); + test_update_does_not_rehash(); + test_custom_hash(); + test_hash_size(); + + if (failures > 0) { + fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + + printf("All tests passed\n"); + return 0; +}