This commit is contained in:
2026-10-29 00:01:50 +01:00
parent 61bb403b08
commit 06f333caea
6 changed files with 586 additions and 163 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
benchmark benchmark
hm_test

View File

@@ -1,8 +1,18 @@
CC=cc
CFLAGS=-std=c11 -Wall -Wextra
CXX=g++ 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 $(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: clean:
rm -rf benchmark rm -f benchmark hm_test
.PHONY: test clean

View File

@@ -1,3 +1,36 @@
# hm.h # 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 <stdio.h>
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;
}
```

View File

@@ -6,6 +6,7 @@
#include <chrono> #include <chrono>
#include <unordered_map> #include <unordered_map>
#include <iomanip> #include <iomanip>
#include <cstring>
#define HM_IMPLEMENTATION #define HM_IMPLEMENTATION
#include "hm.h" #include "hm.h"
@@ -15,16 +16,16 @@ using namespace std::chrono;
typedef time_point<high_resolution_clock> time_p; typedef time_point<high_resolution_clock> time_p;
// Configuration // Configuration
const int NUM_ELEMENTS = 1000000; const size_t NUM_ELEMENTS = 1000000;
time_p now() time_p now()
{ {
return high_resolution_clock::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<nanoseconds>(end - start).count() / 1e6; return duration<double, std::milli>(end - start).count();
} }
void print_div() 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 // Test with string key type
uint64_t hash_str(const void *buffer, size_t size)
{
(void)size;
const char *str = reinterpret_cast<const char*>(buffer);
uint64_t hash = (uint64_t)str;
return hash;
}
void test_str() void test_str()
{ {
std::vector<std::string> key_pool; std::vector<std::string> key_pool;
std::vector<std::string> value_pool; std::vector<std::string> value_pool;
// Generate random strings to ensure we aren't just hashing "1", "2", "3" // 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)); 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)); value_pool.push_back("value_" + std::to_string(i));
} }
// --- Custom implementation --- // --- Custom implementation ---
// Using 0 for sizes tells your library to treat them as NULL-terminated strings // Using 0 for sizes tells the library to treat them as NULL-terminated strings
HashMap hm = hm_create_ex(sizeof(const char *), 0, hash_str); HashMap hm = hm_create(0, 0);
// Insertion // Insertion
auto start = now(); 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()); hm_put(&hm, key_pool[i].c_str(), value_pool[i].c_str());
} }
auto end = now(); 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 // Lookup
start = now(); start = now();
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
char* val = (char*)hm_get(&hm, key_pool[i].c_str()); const char* val = (const char*)hm_get(&hm, key_pool[i].c_str());
if (!val) { if (!val || strcmp(val, value_pool[i].c_str()) != 0) {
std::cerr << "Lookup error!" << std::endl; std::cerr << "Error in custom lookup!" << std::endl;
return; return;
} }
} }
end = now(); end = now();
double custom_get = time_diff_nano(end, start); double custom_get = time_diff_ms(end, start);
// Deletion // Deletion
start = now(); 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()); hm_remove(&hm, key_pool[i].c_str());
} }
end = now(); end = now();
double custom_del = time_diff_nano(end, start); double custom_del = time_diff_ms(end, start);
hm_free(&hm); hm_free(&hm);
// --- Benchmark std::unordered_map --- // --- Benchmark std::unordered_map ---
std::unordered_map<const char*, const char*> std_map; std::unordered_map<std::string, std::string> std_map;
// Insertion // Insertion
start = now(); start = now();
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
std_map[key_pool[i].c_str()] = value_pool[i].c_str(); std_map[key_pool[i]] = value_pool[i];
} }
end = now(); end = now();
double std_put = time_diff_nano(end, start); double std_put = time_diff_ms(end, start);
// Lookup // Lookup
start = now(); start = now();
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
auto it = std_map.find(key_pool[i].c_str()); auto it = std_map.find(key_pool[i]);
if (it == std_map.end()) std::cerr << "Lookup error!" << std::endl; if (it == std_map.end() || it->second != value_pool[i]) {
std::cerr << "Error in std lookup!" << std::endl;
}
} }
end = now(); end = now();
double std_get = time_diff_nano(end, start); double std_get = time_diff_ms(end, start);
// Deletion // Deletion
start = now(); start = now();
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
std_map.erase(key_pool[i].c_str()); std_map.erase(key_pool[i]);
} }
end = now(); end = now();
double std_del = time_diff_nano(end, start); double std_del = time_diff_ms(end, start);
// Output Results // Output Results
print_head(); print_head();
@@ -143,7 +143,7 @@ void test_str()
uint64_t hash_int(const void *buffer, size_t size) uint64_t hash_int(const void *buffer, size_t size)
{ {
(void)size; (void)size;
return *reinterpret_cast<const int*>(buffer); return *reinterpret_cast<const unsigned int*>(buffer);
} }
void test_int() void test_int()
@@ -152,9 +152,9 @@ void test_int()
std::vector<int> keys(NUM_ELEMENTS); std::vector<int> keys(NUM_ELEMENTS);
std::vector<int> values(NUM_ELEMENTS); std::vector<int> values(NUM_ELEMENTS);
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
keys[i] = i; keys[i] = static_cast<int>(i);
values[i] = i * 2; values[i] = static_cast<int>(i * 2);
} }
// --- Custom implementation --- // --- Custom implementation ---
@@ -162,15 +162,15 @@ void test_int()
// Insertion // Insertion
auto start = now(); 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]); hm_put(&hm, &keys[i], &values[i]);
} }
auto end = now(); auto end = now();
double custom_put = time_diff_nano(end, start); double custom_put = time_diff_ms(end, start);
// Lookup // Lookup
start = now(); 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]); int* val = (int*)hm_get(&hm, &keys[i]);
if (!val || *val != values[i]) { if (!val || *val != values[i]) {
std::cerr << "Error in custom lookup!" << std::endl; std::cerr << "Error in custom lookup!" << std::endl;
@@ -178,15 +178,15 @@ void test_int()
} }
} }
end = now(); end = now();
double custom_get = time_diff_nano(end, start); double custom_get = time_diff_ms(end, start);
// Deletion // Deletion
start = now(); start = now();
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
hm_remove(&hm, &keys[i]); hm_remove(&hm, &keys[i]);
} }
end = now(); end = now();
double custom_del = time_diff_nano(end, start); double custom_del = time_diff_ms(end, start);
hm_free(&hm); hm_free(&hm);
@@ -195,30 +195,30 @@ void test_int()
// Insertion // Insertion
start = now(); 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]; std_map[keys[i]] = values[i];
} }
end = now(); end = now();
double std_put = time_diff_nano(end, start); double std_put = time_diff_ms(end, start);
// Lookup // Lookup
start = now(); 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]); auto it = std_map.find(keys[i]);
if (it == std_map.end() || it->second != values[i]) { if (it == std_map.end() || it->second != values[i]) {
std::cerr << "Error in std lookup!" << std::endl; std::cerr << "Error in std lookup!" << std::endl;
} }
} }
end = now(); end = now();
double std_get = time_diff_nano(end, start); double std_get = time_diff_ms(end, start);
// Deletion // Deletion
start = now(); start = now();
for (int i = 0; i < NUM_ELEMENTS; ++i) { for (size_t i = 0; i < NUM_ELEMENTS; ++i) {
std_map.erase(keys[i]); std_map.erase(keys[i]);
} }
end = now(); end = now();
double std_del = time_diff_nano(end, start); double std_del = time_diff_ms(end, start);
// Output Results // Output Results
print_head(); print_head();

317
hm.h
View File

@@ -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. // chained hash table implementation as a single header library.
// //
// [License and changelog] // [License and changelog]
// //
// See end of file. // 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 <stdio.h>
//
// 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] // [Single header library usage]
// //
// Including this header file as such does what you normally expect. // Including this header file as such does what you normally expect.
@@ -24,12 +68,12 @@
// #define HM_INITIAL_CAPACITY new_initial_capacity (256) // #define HM_INITIAL_CAPACITY new_initial_capacity (256)
// //
// This macro defines the initial amount of buckets a HashMap will // 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) // #define HM_DEFAULT_HASH_FUNCTION new_hash_function (hm__fnv1a)
// //
// This macro defines the hash function that will be used by default // 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 // The specified function has to respect the HmHashFunc type
// definition signature. // definition signature.
// //
@@ -41,7 +85,16 @@
// #define HM_GROWTH_FACTOR new_growth_factor (2) // #define HM_GROWTH_FACTOR new_growth_factor (2)
// //
// This macro defines by how much should a HashMap be reallocated when // 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 // #define HM_NO_ASSERT
// //
@@ -49,6 +102,11 @@
// fail. The disabled assertions will be replaced with a simple // fail. The disabled assertions will be replaced with a simple
// condition that returns false or NULL when they fail. // 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_REALLOC my_realloc (realloc)
// #define HM_FREE my_free (free) // #define HM_FREE my_free (free)
// //
@@ -71,23 +129,31 @@
// This functions initializes a HashMap structure with the specified key and // 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 // 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 // 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) // 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 // 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) // void hm_free(HashMap *hm)
// //
// This functions frees and invalidates all the keys and values stored in the // 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) // bool hm_put(HashMap *hm, const void *key, const void *value)
// //
// This function inserts a new value associated with the specified key. If the // 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 // 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 // WARNING: Passing a 'value' pointer that references memory currently stored
// within the HashMap (self-referencing update) results in undefined // within the HashMap (self-referencing update) results in undefined
@@ -96,7 +162,8 @@
// void *hm_get(const HashMap *hm, const void *key) // void *hm_get(const HashMap *hm, const void *key)
// //
// This function returns the value associated with the specified key. Returns // 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) // type *hm_of(type, hm, key)
// //
@@ -119,6 +186,10 @@
// HashMap. Note that the returned iterator does not start from the first // 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. // 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) // const void *hm_key(const HashMapIterator *it)
// //
// This function returns the key of the current entry. Returns NULL if the // This function returns the key of the current entry. Returns NULL if the
@@ -141,6 +212,7 @@
#ifndef HM_H_ #ifndef HM_H_
#define HM_H_ #define HM_H_
#include <stdalign.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@@ -162,12 +234,17 @@
# define HM_GROWTH_FACTOR 2 # define HM_GROWTH_FACTOR 2
#endif // HM_GROWTH_FACTOR #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_NO_ASSERT
# ifndef HM_ASSERT # ifndef HM_ASSERT
# include <assert.h> # include <assert.h>
# define HM_ASSERT assert # define HM_ASSERT assert
# endif // HM_ASSERT # endif // HM_ASSERT
#else #else
# undef HM_ASSERT
# define HM_ASSERT(...) ((void)0) # define HM_ASSERT(...) ((void)0)
#endif // HM_NO_ASSERT #endif // HM_NO_ASSERT
@@ -181,11 +258,8 @@
# define HM_FREE free # define HM_FREE free
#endif // HM_FREE #endif // HM_FREE
#ifdef __cplusplus // Type definition for a hash function returning a 64-bit hash. For NULL
extern "C" { // Prevent name mangling // terminated keys 'size' does not include the terminator.
#endif // __cplusplus
// Type definition for a hash function returning a 64-bit hash
typedef uint64_t (*HmHashFunc)(const void *buffer, size_t size); typedef uint64_t (*HmHashFunc)(const void *buffer, size_t size);
typedef struct Hm__Bucket { typedef struct Hm__Bucket {
@@ -194,6 +268,7 @@ typedef struct Hm__Bucket {
void *key; void *key;
void *value; void *value;
size_t value_size; size_t value_size;
uint64_t hash;
} Hm__Bucket; } Hm__Bucket;
typedef struct { typedef struct {
@@ -212,6 +287,10 @@ typedef struct {
bool end; bool end;
} HashMapIterator; } HashMapIterator;
#ifdef __cplusplus
extern "C" { // Prevent name mangling
#endif // __cplusplus
// Create and free // Create and free
HashMap hm_create(size_t key_size, size_t value_size); 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); 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 // Modify, access and remove
bool hm_put(HashMap *hm, const void *key, const void *value); bool hm_put(HashMap *hm, const void *key, const void *value);
void *hm_get(const HashMap *hm, const void *key); 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); bool hm_remove(HashMap *hm, const void *key);
size_t hm_count(const HashMap *hm); 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_value(const HashMapIterator *it);
void *hm_next(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 #ifdef __cplusplus
} }
#endif // __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 HM_IMPLEMENTATION
#ifdef __cplusplus // Private functions
extern "C" { // Prevent name mangling uint64_t hm__fnv1a(const void *buffer, size_t size);
#endif // __cplusplus 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) HashMap hm_create(size_t key_size, size_t value_size)
{ {
@@ -280,17 +358,9 @@ void hm_free(HashMap *hm)
return; return;
} }
if (hm->map == NULL) { if (hm->map != NULL) {
memset(hm, 0, sizeof(*hm));
return;
}
for (size_t i = 0; i < hm->capacity; ++i) { for (size_t i = 0; i < hm->capacity; ++i) {
Hm__Bucket *cur = hm->map[i]; Hm__Bucket *cur = hm->map[i];
if (cur == NULL) {
continue;
}
while (cur != NULL) { while (cur != NULL) {
Hm__Bucket *next = cur->next; Hm__Bucket *next = cur->next;
HM_FREE(cur); HM_FREE(cur);
@@ -299,7 +369,12 @@ void hm_free(HashMap *hm)
} }
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) 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); 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 // Key-value sizes
size_t key_size = hm__key_size(hm, key); size_t key_size = hm__key_size(hm, key);
size_t value_size = hm__value_size(hm, value); size_t value_size = hm__value_size(hm, value);
uint64_t hash = hm__hash(hm, key, key_size);
// Get the head bucket
size_t idx = hm->hash_func(key, key_size) % hm->capacity;
Hm__Bucket *head = hm->map[idx];
// Check if key already exists // Check if key already exists
for (Hm__Bucket *cur = head; cur != NULL; cur = cur->next) { Hm__Bucket **slot = &hm->map[(size_t)hash & (hm->capacity - 1)];
if (!hm__keycmp(hm, key, cur->key)) { for (Hm__Bucket *cur = *slot; cur != NULL; cur = cur->next) {
if (cur->hash != hash || !hm__keycmp(hm, key, cur->key)) {
continue; continue;
} }
@@ -348,7 +414,8 @@ bool hm_put(HashMap *hm, const void *key, const void *value)
} }
// Reallocate the entire bucket // 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); Hm__Bucket *new_bucket = (Hm__Bucket*)HM_REALLOC(cur, size);
if (new_bucket == NULL) { if (new_bucket == NULL) {
HM_ASSERT(false && "Reallocation failed"); HM_ASSERT(false && "Reallocation failed");
@@ -356,8 +423,8 @@ bool hm_put(HashMap *hm, const void *key, const void *value)
} }
// Recalculate key-value pointers // Recalculate key-value pointers
new_bucket->key = (uint8_t*)new_bucket + sizeof(*new_bucket); new_bucket->key = (uint8_t*)new_bucket + key_offset;
new_bucket->value = (uint8_t*)new_bucket->key + key_size; new_bucket->value = (uint8_t*)new_bucket + value_offset;
new_bucket->value_size = value_size; new_bucket->value_size = value_size;
// Update linked list pointers of neighbors // 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) { if (new_bucket->prev != NULL) {
new_bucket->prev->next = new_bucket; new_bucket->prev->next = new_bucket;
} else { } else {
size_t idx = hm->hash_func(key, key_size) % hm->capacity; *slot = new_bucket;
hm->map[idx] = 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; 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 // 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) { if (new_bucket == NULL) {
return false; return false;
} }
size_t idx = (size_t)hash & (hm->capacity - 1);
Hm__Bucket *head = hm->map[idx];
new_bucket->next = head; new_bucket->next = head;
if (head != NULL) { if (head != NULL) {
head->prev = new_bucket; 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 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]; for (Hm__Bucket *cur = hm->map[idx]; cur != NULL; cur = cur->next) {
if (head == NULL) { if (cur->hash == hash && hm__keycmp(hm, key, cur->key)) {
return NULL;
}
for (Hm__Bucket *cur = head; cur != NULL; cur = cur->next) {
if (hm__keycmp(hm, key, cur->key)) {
return cur->value; 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 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]; for (Hm__Bucket *cur = hm->map[idx]; cur != NULL; cur = cur->next) {
if (head == NULL) { if (cur->hash != hash || !hm__keycmp(hm, key, cur->key)) {
return false;
}
for (Hm__Bucket *cur = head; cur != NULL; cur = cur->next) {
if (!hm__keycmp(hm, key, cur->key)) {
continue; continue;
} }
if (cur == head) { if (cur->prev == NULL) {
hm->map[idx] = cur->next; hm->map[idx] = cur->next;
} else { } else {
cur->prev->next = cur->next; cur->prev->next = cur->next;
@@ -532,27 +604,14 @@ void *hm_next(HashMapIterator *it)
return NULL; 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 // Advance within current chain
if (it->bucket->next != NULL) { if (it->bucket != NULL && it->bucket->next != NULL) {
it->bucket = it->bucket->next; it->bucket = it->bucket->next;
return it->bucket->value; 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) { while (it->index < hm->capacity) {
it->bucket = hm->map[it->index++]; it->bucket = hm->map[it->index++];
if (it->bucket != NULL) { if (it->bucket != NULL) {
@@ -573,8 +632,6 @@ uint64_t hm__fnv1a(const void *buffer, size_t size)
return 0; return 0;
} }
// TODO: When buffer is NULL terminated, the NULL is included in the hash
const uint8_t *b = (const uint8_t*)buffer; const uint8_t *b = (const uint8_t*)buffer;
uint64_t hash = 14695981039346656037ULL; uint64_t hash = 14695981039346656037ULL;
@@ -586,6 +643,21 @@ uint64_t hm__fnv1a(const void *buffer, size_t size)
return hash; 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) size_t hm__key_size(const HashMap *hm, const void *key)
{ {
if (hm == NULL || key == NULL) { if (hm == NULL || key == NULL) {
@@ -641,9 +713,7 @@ bool hm__rehash(HashMap *hm)
Hm__Bucket *cur = hm->map[i]; Hm__Bucket *cur = hm->map[i];
while (cur != NULL) { while (cur != NULL) {
Hm__Bucket *next = cur->next; Hm__Bucket *next = cur->next;
size_t idx = (size_t)cur->hash & (new_capacity - 1);
size_t key_size = hm__key_size(hm, cur->key);
uint64_t idx = hm->hash_func(cur->key, key_size) % new_capacity;
// Insert new bucket // Insert new bucket
cur->prev = NULL; cur->prev = NULL;
@@ -664,16 +734,41 @@ bool hm__rehash(HashMap *hm)
return true; 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"); HM_ASSERT(false && "Invalid parameters");
return NULL; return NULL;
} }
// Allocate bucket // Allocate bucket
// [ Hm__Bucket ][ key ][ value ] size_t key_offset, value_offset;
size_t size = sizeof(Hm__Bucket) + key_size + value_size; size_t size = hm__bucket_layout(hm, key_size, value_size, &key_offset, &value_offset);
Hm__Bucket *bucket = (Hm__Bucket*)HM_REALLOC(NULL, size); Hm__Bucket *bucket = (Hm__Bucket*)HM_REALLOC(NULL, size);
if (bucket == NULL) { if (bucket == NULL) {
HM_ASSERT(false && "Reallocation failed"); 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)); memset(bucket, 0, sizeof(*bucket));
// Prepare key-value pointers offset // Prepare key-value pointers offset
bucket->key = (uint8_t*)bucket + sizeof(*bucket); bucket->key = (uint8_t*)bucket + key_offset;
bucket->value = (uint8_t*)bucket->key + key_size; bucket->value = (uint8_t*)bucket + value_offset;
bucket->value_size = value_size; bucket->value_size = value_size;
bucket->hash = hash;
// Copy key-value data // Copy key-value data
memcpy(bucket->key, key, key_size); 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; return bucket;
} }
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // HM_IMPLEMENTATION #endif // HM_IMPLEMENTATION
/* /*
* Revision history: * 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 * 1.2.0 (2026-04-08) New HmHashFunc type, configurable by the user with
* hm_create_ex() or HM_DEFAULT_HASH_FUNCTION * hm_create_ex() or HM_DEFAULT_HASH_FUNCTION
* 1.1.2 (2026-04-08) Optimized Hm__Bucket creation by only allocating one * 1.1.2 (2026-04-08) Optimized Hm__Bucket creation by only allocating one

268
test.c Normal file
View File

@@ -0,0 +1,268 @@
#define HM_IMPLEMENTATION
#include "hm.h"
#include <stdio.h>
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;
}