v1.3.0
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
benchmark
|
benchmark
|
||||||
|
hm_test
|
||||||
|
|||||||
16
Makefile
16
Makefile
@@ -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
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|||||||
100
benchmark.cpp
100
benchmark.cpp
@@ -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
317
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.
|
// 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
268
test.c
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user