An easy-to-implement, arena-friendly hash map
nullprogram.com
nullprogram.com
discussed here :
https://news.ycombinator.com/item?id=33379079
https://old.reddit.com/r/programming/comments/xmnfo8/untangl...
https://old.reddit.com/r/C_Programming/comments/xmxmex/untan...
PS:it also ties with forth different stacks. Interesting design space.
The original series of articles had me thinking about how to implement the normal hash table without leaking. I think it’s possible:
First consider a hash table using linear probing with a dedicated arena (restriction will be lifted later). That means memory can grow for free. However, resizing is still tricky because we need to rehash in place while ensuring we don’t “strand” any entries. This can be accomplished by iterating through the hash table entries in order, but starting at an empty entry and looping around. (Proof left as an exercise for the reader)
So now we have a hash table that can use realloc to grow. How to generalize for arenas? The key is that the HT will maintain a page map of sorts. The first page is of size M, the second is of size M, and thereafter they’ll be of size 2^n * M. Basically on each resize we double the total capacity by adding a new page. Since the pages are of variable width, though, how to map indexes to pages quickly? The key is to use ffs or ctz to get a rounded log2.
#include <stdint.h>
#include <math.h>
#include <stdbool.h>
#define BASE_PAGE_BITS 14
#define MAX_PAGES 22
struct KeyValue { keytype key; valtype val; bool inUse; };
uint32_t hash(keytype key);
int equals(keytype key1, keytype key2);
struct HashTable {
struct KeyValue pages[MAX_PAGES][];
uint8_t usedPages;
};
typedef struct HashTable HashTable;
struct KeyValue* lookup(HashTable* table, keytype key) {
uint32_t hashed = hash(key);
uint32_t lowerBits = hashed & ((1 << (BASE_PAGE_BITS + table->usedPages)) - 1);
uint32_t pageIndex = (lowerBits == 0) ? 0 : (uint32_t)(log2(lowerBits >> BASE_PAGE_BITS) + 1);
uint32_t entryIndex;
for (;;) {
uint32_t entryIndexRange = (pageIndex == 0) ? (1 << BASE_PAGE_BITS) : (1 << (pageIndex + BASE_PAGE_BITS - 1));
for (; entryIndex < entryIndexRange; entryIndex++) {
struct KeyValue* page = table->pages[pageIndex];
if (!page[entryIndex].inUse) return &page[entryIndex];
if (equals(page[entryIndex].key, key)) return &page[entryIndex];
}
pageIndex = (pageIndex + 1) % table->usedPages;
entryIndex = 0;
}
}Yes, I thought this was clear from my statement.
“Rotation would not make a difference”
It would. The offsets generated by the hash would repeat every 32 shifts, but the absolute addresses given in the collision cases are a random construction based on the history of the tree at that point, so despite the offsets’ repeating, the tree’s invariants along the lookup are likely to be preserved.
Yeah, sorry, wasn't really necessary to repeat that point. I was too focused on the "limiting the height of the tree to 32" formulation.
I have to admit that I don't quite understand what invariants along the lookup you mean. All lookups that reach a particular node on level 32 have the same hash, so regardless of how you compute the branch from the hash below level 32, they will always follow the same path starting from there (except for terminating at different levels, obviously). So nodes only ever have one child at most, and there's no loss in simply picking child 0 in all cases.
Sorry if what I'm describing is again obvious, maybe I just don't understand your point correctly.
I've written about how they can not only be used for hashes but also for storing sparse indexes. I'm hoping to write an extremely tiny Lua implementation which uses only slab allocation (even better than arena IMO! Though I do love arena allocators)
https://github.com/civboot/civboot/blob/main/blog/0013-civbo...