How to implement a hash table in C
benhoyt.com
benhoyt.com
Yes, but how you resize is important too: if you have a threshold size (like 3/4 full) at which you block and re-distribute all elements into a new array, you will incur a significant pause when this happens, e.g. https://log.kv.io/post/2009/05/15/leaky-hashtables
Instead, when you reach the threshold amount you can create the new array and then gradually migrate the keys in small batches either with each operation or with a background thread. So on `get` if we're in the process of resizing: first check the new table, then the old one, and before returning migrate N keys from the old table to the new one. Free the old array once all the keys are migrated.
I wrote a small hash table implementation with gradual re-hashing a while back, search for DICT_MAX_LOAD and dict_rehash here: https://github.com/nicolasff/ht/blob/master/dict.c#L193
Hash tables generally work on the assumption that operations are O(1) in the amortized sense, that individual inserts might be expensive because of rehashing, but in aggregate they are very cheap. In practice, this is almost always a fine assumption, and it is how the vast majority of hash tables in all programming languages work.
The kinds of hash tables you are talking about have their uses in places where you have soft real-time constraints, but that is honestly a pretty niche use case.
Hash tables aren't O(1) in an amortized sense, they are O(1) in a statistical sense. The O(1) complexity of hash tables is dependent entirely on the size of the underlying array, the method of collision resolution, and the quality of the (pre)hash function in relation to the distribution of data you're storing in the table. Whether you insert 1 item or 1 million items into the table, you're still getting O(1) computational complexity if the table is designed well. If not, you could be getting O(n) complexity in the worst case.
While the fixed cost of hashing is irrelevant in the O(1) nature of hash table operations, it does mean that a hash table in practice could be slower (in terms of wall time) than a linked list for insert/find/remove operations. Despite a linked list being O(n) for those operations, a linked list doesn't have to pay the fixed cost of hashing. So for low values of n, the linked list might be a better choice. I think this is what you mean by "O(1) in the amortized sense" but it's not what we mean when we say the computational complexity of these operations on a hash table is O(1).
What the OP is talking about is an example of amortization of resize(), essentially adding a fixed cost to insert() and remove() so you don't have to pay a huge sum when you call resize(). But that doesn't mean insert() and remove() are no longer O(1).
If you use the terminology precisely, the worst case cost of a regular hash table insertion is O(n) because of the potential resize, but its amortized cost is O(1) because that's what it "averages out to".
Just thought I'd share a example of a use case where the incremental rehashing logic makes sense.
[1]: https://github.com/redis/redis/blob/unstable/src/dict.c
Imagine hash tables used to store configuration data. You don't care about performance of "reload config" operation, but once config is loaded and the worker is handling traffic, read operations must be fast.
Not just get, but set also, right? Otherwise I would think a large group of set operations that overflowed more than once might be problematic to deal with. Or maybe I'm missing something that makes that a non issue?
Do you now end up with three live arrays? You can probably make things even more pathological...
Rehashing is controlled by various tunable strategies: Load factor (0.5-1), growth policy (primed or power2), growth factor (1.5, golden ratio or 2).
Linked list tables don't need to throw away nodes, they can just be relinked. But compaction is always better, because cache dominates. Most rehash do exactly that. Esp. multithreaded.
The best thing to do is to take a good one and don't touch it, unless you want a monster as in perl5. Eg the recent siphash security theater broke performance in all dynamic languages, even the linux kernel. Everything is perl now. You don't want that.
My comment was a description of a precise “drastic change” where incrementality becomes sophisticated. (Though in part I was hoping for a response which identifies an elegant approach without the complication I mentioned.)
It was in either the 1st (pre-ANSI C) or the 2nd (ANSI C) edition. Just a handful of lines of C code, and easy enough for even beginners to understand. Of course, it was not a production-quality hash table implementation, nor was it meant to be. I still remember my excitement at understanding the logic and its usefulness at the time, even though I did not know about the importance of data structures and algorithms then.
Edit: Even though it was simple, IIRC, it handled at least collisions.
Pun not intended but noticed, ha ha.
[1] https://attractivechaos.wordpress.com/2019/12/28/deletion-fr...
> I think the new deletion algorithm without tombstones is overall better than traditional algorithms. It should become the preferred way to implement hash tables
I agree with you 100%. Tombstone has no advantage over this.
Tombstones are silly, you do add/remove %75 of your map capacity, now you have to rehash your map even your map is empty. I can’t think a single scenario that tombstones can perform better. It causes much more probing and much more cache misses than backshift deletion.
> The advantage of tombstones is that it is dead simple to implement.
Backshift is harder to understand but it actually takes fewer LOC to implement at my hand. That is because insertion and searching become simpler (and slightly faster) with backshift.
> Backshift removals can cause a lot of memory shuffling
Not sure what you mean by memory shuffling. Backshift is applied to linear probing only. You don't jump around except when you wrap around the end of the bucket array, which is rare.
I would wager that is true in most cases.
> This slows down searching and insertion as well as deletion. Tombstones also increase the frequency of rehashing.
Yes, but you could argue that is an advantage; the hash table reaches its "natural size" faster.
> Backshift is harder to understand but it actually takes fewer LOC to implement at my hand. That is because insertion and searching become simpler (and slightly faster) with backshift.
I looked at your code and, afaict, it only supports linear probing. Backshift removals would be much harder or maybe even impossible to implement with quadratic probing or double hashing.
Strings. In C, comparing two strings involves a function call, a loop and a potential cache miss. Copying a string just moves a pointer, which is much cheaper. The additional copying costs little for strings.
> the hash table reaches its "natural size" faster.
Not sure what you mean by this. The "nature size" is the size without tombstones. Clearly backshift is the better one.
> it only supports linear probing
Yes, backshift only supports linear probing so far as I know. Most high-performance hash table libraries use linear probing these days. A few use quadratic probing. I rarely see double hashing.
Anyway, backshift always uses less memory and is always faster on insertions and searching. Under certain deletion load, backshift may be slower due to 1) and 2), but the difference is fairly small considering the save on memory.
A better approach would be to store the string's hashcode somewhere and only perform the element-wise comparison in case they match. For most strings they are unlikely to match.
> Not sure what you mean by this. The "nature size" is the size without tombstones. Clearly backshift is the better one.
You can count the fraction of the table consumed by tombstones and grow it a little earlier if that fraction is significant.
> Yes, backshift only supports linear probing so far as I know. Most high-performance hash table libraries use linear probing these days. A few use quadratic probing. I rarely see double hashing.
Yes, but you are trading amazing performance in the average case against awful performance in the degenerate case. Poor hash functions or adversarial input could cause congestion in parts of the hash table.
It is not just me. As I said, most high-performance hash tables these days (absl, F14, and all robin hood implementations) use linear probing. Providing a proper default hash function avoids most worse cases in practice. In addition, the hash library can use a secondary hash function [1] which makes the library much more robust to bad hash functions such as the identity hash function.
[1] https://attractivechaos.wordpress.com/2018/10/01/advanced-te...
> The basic idea is not complex: when we delete an element, we move the next element to the deleted location if it is pushed away by the deleted element due to hash collision; we repeat this process until we come to an empty bucket.
I wonder, is it still amortized constant time? My gut (which is not very good at computer science) tells me that it still may be, as with an average probe length of ~1.4 (which I was seeing), you won't have to walk more than 1 or 2 elements on average.
I had wanted to use "better" techniques like quadratic probing, but I couldn't figure out how to perform deletions, so I fell back on linear probing.
All I'm trying to say is if you want to know more about hash tables, and you bother to venture into the literature or some textbooks, you're going to run into this distinction sooner rather than later, so I wanted to point that out here.
Articles like this are really useful for someone who wants to under the hood.
Nasty stuff that you can do in a language like C :)
> Here’s how you’d do it in C (with and without bsearch).
Juding by random hit on the net - it would've been quite straightforward to use the stdlib version?
https://www.tutorialspoint.com/c_standard_library/c_function...
In typical Unix fashion, however, it is designed to use internal, global state, so you can only have one table at a time.
There is a GNU extension that provides the same functions with an `_r` suffix that allow you to work with more than one table.
https://en.cppreference.com/w/c/string/byte/strtok
Was there some reason it was optimal to write these functions this way back in the early days of Unix?
Plus you weren't going to need more than one anyway, your program wasn't going to be big enough.
As long as you don't have multiple threads (and you shouldn't, and on Unix you couldn't) the static-variable interface in these cases was usually more convenient, though more bug-prone.
There's also a micro-efficiency question.
Global variables are at a known memory address. If you put them in a struct and pass around a pointer to it, they are at a dynamically computed memory address. Every time you want to access them, you have to add the field offset to the struct's base pointer. This is a small amount of extra computation, and how much it actually costs you depends on the hardware—on the 8080 it's a mess that probably requires you to spill a couple of registers onto the stack, and on modern implementations of amd64 like the ones from Intel, it probably takes literally zero extra time, because you probably aren't keeping all of your addition-capable functional units busy anyway.
I don't know the PDP-11's instruction set well enough to say, but I suspect the cost of such indexed addressing was intermediate between these extremes: much like the 8086, it had an "index" addressing mode, mode 6, which added an immediate 16-bit offset to a base register https://pages.cpsc.ucalgary.ca/~dsb/PDP11/AddrModes.html, and it doesn't seem to have had a pure immediate addressing mode where the address of the variable is just an immediate word without any indexing. But if you didn't need a base register for your variables, you could use PC-relative addressing, thus saving a register and decreasing register pressure (the PDP-11 only had 8 general-purpose registers, and one of them was PC, so it really only had 7).
I don't have any idea what actual PDP-11 compilers did, though, and that matters a lot here.
https://github.com/ludocode/pottery/tree/master/examples/pot...
Be warned, hsearch is terrible. You should probably only use this if you have some existing code that depends on hsearch and you want to make it portable.
I think this is still UB to allocate a struct containing pointer with calloc without explicitly set these pointers to 0 (or NULL). The C standard doesn't specify the binary representation of the null pointer value. In practice, that will work in virtually all current platforms, but it's still UB.
Anywhere that they technically could, you can treat it as if they actually had so declared, and just didn't publicize it enough.
edit:
I haven't found any smoking gun quotation in the C11 standard on short notice, but there is footnote 296 (p.348) on calloc() zeroing all bits:
> Note that this need not be the same as the representation of floating-point zero or a null pointer constant.
Nothing about this being UB in cases where it does represent a null pointer constant.
> An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
See also these FAQs:
So, no, in practice it's well-defined, fully specified behavior on any given platform.
I guess what I want is a struct like data structure, but in an interpreted language
Mightve typed to fast...
Python ffi is an option: https://cffi.readthedocs.io/en/latest/using.html#working-wit...
Though I don't know what advantage that route would have over most interpreted language's already existing associative arrays / hashmaps / dicts.
There may be some module already built that serves your needs. DiscoDB is a good example..a very fast hashmap that uses mmap(). https://discodb.readthedocs.io/en/latest/
I got thrown off by "but in an interpreted language" so I'm not sure whether you're requesting this for C or for another language. In JS, at least the V8 engine does some of this for you: if you have a group of objects that always have the same set of properties, V8 will detect this in many cases and optimize them under the hood as a struct or class (it must also store the string keys somewhere since those can be iterated in JS).
In C/C++ you'd have to store the keys as strings somehow, and also add some way to index the struct via those keys (since you can't even do that with a normal struct). You may be stuck with a hashmap one way or another.
[0] https://llvm.org/docs/CodingStandards.html#do-not-use-rtti-o...
[1] https://google.github.io/styleguide/cppguide.html#Run-Time_T...
It also depends on how you can represent the string and their typical size. If the strings are small, then allocating fixed size chunks to store them inline might make sense. If the strings are long, it might make sense to go crazy and use a "trie" data structure to minimize comparisons.
I believe python uses hash maps for classes by default, but you can alternatively use "slots", which are less mutable and presumably more densely packed.
Essentially, you have an array/list/vector under the hood, and a mapping of field names to array indices to govern field access.
If you're trying to implement this at a lower level, in the interpreted language itself, take a look at the slots mechanism[2] in Python.
[1]: https://docs.python.org/3/library/collections.html#collectio...
[2]: https://docs.python.org/3/reference/datamodel.html#object.__...
You can use a static instance if you just need a single object.
size_t mid = (low + high) / 2;
if (size + size < size) {
return NULL; // size too big; avoid overflow
}Since this is integer division, I think you can do:
size_t low_s = low >> 1;
size_t high_s = high >> 1;
size_t mid = (low_s + high_s)
...With no overflow worries. (expanded for clarity)That would effectively be distributing the division. So it's equivalent to low / 2 + high / 2.
set: the worst. unordered_set: the worst. vector: no small vector optimizations. string: horrible. array: use small_vector instead. deque: slow. list and forward_list: nobody uses that for performance.
That's why it's important to pick the right hash function. A hash table usually goes a long way before it becomes a performance issue.
1) Slower than hash tables
2) `{}` is not empty but has default keys
3) `{}` has multiple read-only keys
2 and 3 are caused by the prototype chain (properties not present on the initial object will be attempted to be resolved via the prototype hierarchy) which can be avoided via `Object.create(null)`
- There is only one global hash table for the whole process! If you want individual hash tables you need to use implementation-specific extensions (hcreate_r().)
- There is no way to remove a key from a hash table. No implementation I know of provides an extension to do it. If you want to truly remove a key you must destroy and rebuild the table.
- There is no requirement for a means to grow the table. On some platforms it's possible to run out of space. If you want to truly grow you must destroy and rebuild the table.
- There is no standard way to free keys or data stored in the table. Destroying the table leaks all contents; you must keep track of keys and data externally and free them yourself. Only OpenBSD frees keys by default, and only NetBSD has extensions to call callbacks to free keys and data.
- Keys must be strings. You cannot provide custom types or void pointers as keys. There is no way to specify custom hash or comparison functions. The quality of the hash function is unspecified.
- When using ENTER, you may not want to allocate the key unless it is necessary to insert a new entry. Since the given key is inserted directly, it's not necessarily safe to use a stack-allocated buffer for lookups. It's awkward to replace the key with an allocation after insertion and it's unclear whether it's allowed.
This doesn't even get into incompatibilities between the various implementations. You will encounter all of the above flaws even if your only target is glibc.
No one should ever be encouraged to use POSIX hash tables. They should be deprecated and we should all just pretend they don't exist.
Or at the very least just deprecate the old one. I can't remember ever seeing it used in code.
Yes, I realize the lookup would get complicated.
There is a GNU extension that lets you have more than one table.
A merge sort isn't the fastest sort but it's pretty easy for anyone to implement. Something like a timsort is a bit more complex but is what a lot of languages use now-a-days.
Hash tables are in the same boat. A basic hashtable is pretty easy to implement. It's only when you start looking at things like ideal item placement and optimal filling that things start to get more complex.
1. It doesn't update—attempts to change the value associated with an existing key are silently ignored, although they do consume buckets.
2. It loops forever if the table is full and the key it was looking for is negative, because that invokes conversion of a negative signed value to an unsigned value, which I think is UB in C.
Maybe it has more bugs I haven't found yet.
Additionally the reason it took me 15 minutes to write 21 lines of code was that I changed a bunch of things in midstream, before posting the comment:
1. At first there was no "full" field in the hash table bucket, so there was no way to distinguish full buckets from empty buckets.
2. And then when I added it, I added it as "dead", with the opposite Boolean sense of "full", so I would have needed a separate initialization routine (to set all the dead fields to something nonzero). Instead I swapped the sense.
3. I was trying to detect the case where we'd looped back around to our starting point, indicating that the table was full and the search was unsuccessful, and I realized that the condition I wanted was not b == orig, which would always fail on the first iteration, but (b + 1) % table_size == orig. But doing that on every iteration seemed obscene, so I incremented b (mod table_size) to start out with instead.
4. At some point I think I realized I'd forgotten to set the .full field on newly inserted items.
5. Also I realized I'd forgotten to test the table[b].k == k condition in `get` as well, which would have made every search unsuccessful.
So I think it's reasonable to say that even a pretty limited hash-table implementation has plenty of opportunities to write bugs, more than I expected. But maybe I'm just running on low blood sugar this afternoon or something.
let rec getp k = function `Empty -> None
| `Node(k1, v1, x, y) ->
if k1 = k then Some v1 else getp k (if k < k1 then x else y)
and putp k v = function `Empty -> `Node(k, v, `Empty, `Empty)
| `Node(k1, v1, x, y) when k1 = k -> `Node(k, v, x, y)
| `Node(k1, v1, x, y) -> if k < k1 then `Node(k1, v1, putp k v x, y)
else `Node(k1, v1, x, putp k v y)
And it's about a third the size of the hash-table code. OTOH I spent the last hour writing it.With four more lines of code it's also a sorting algorithm:
and dictofp = function [] -> `Empty | (k, v)::xs -> putp k v (dictofp xs)
and itemsp d = let rec iter d tl = match d with `Empty -> tl
| `Node(k, v, x, y) -> iter x ((k, v) :: iter y tl)
in iter d []
Look: # List.map (fun (x, y) -> x) (itemsp (dictofp (List.map (fun x -> (x, x)) ["this"; "is"; "a"; "list"; "of"; "strings"])));;
- : string list = ["a"; "is"; "list"; "of"; "strings"; "this"]
Of course the same comments about worst-case performance apply... enum { table_size = 1024 };
typedef struct item { int k, v, full; } item;
static item table[table_size];
void put(item kv)
{
size_t orig = kv.k % table_size; // dumbest possible hash function
size_t b = (orig + 1) % table_size;
// use linear probing for collisions
while (table[b].full && b != orig) b = (b + 1) % table_size;
if (b == orig) abort(); // table full
table[b] = kv;
table[b].full = 1;
}
item *get(int k)
{
size_t orig = k % table_size;
size_t b = (orig + 1) % table_size;
while (table[b].full && table[b].k != k && b != orig) {
b = (b + 1) % table_size;
}
return (table[b].full && table[b].k == k) ? &table[b] : NULL;
}
I haven't tested that (just as I wouldn't in a whiteboard interview) so I don't know if it works. I did briefly skim the article we're talking about, but I concluded I didn't have anything to learn from it, so I didn't read it. I found a bunch of bugs in my implementation as I was writing it. And of course it's a pretty dumb hash table: linear probing is very suboptimal, it uses a statically-allocated non-resizable hash table, it's specialized for a certain type of keys, and so on.But are you saying you can't even do that? Probably it would be bad to hire you for a C programming job, then, unless it was an entry-level position for you to learn C in.
⁂
Evidently the above comment took me 15 minutes to write. Oh, and now I see another bug or at least lacuna: if you try to update the value of an existing key, it doesn't fail, but it also doesn't update, instead adding a new entry that will never be read. And then I did write a minimal smoke test and run it, and unsurprisingly, it does work:
int main()
{
put((item) { 4, 7 });
put((item) { 1028, 9 });
put((item) { 3, 25 });
printf("%d %d %d %p %p\n", get(4)->v, get(1028)->v, get(3)->v, get(5), get(3));
return 0;
}
Writing the test and the above additional commentary, and running the test, took another 8 minutes.Oh, and there's another bug where it will loop infinitely when the hash table is full and the key is negative. (Actually it invokes UB but typically the result will just be an infinite loop.)
If it takes you 60 or 100 minutes to write this, or to write something better, maybe you still know C and know what hash tables are. But if you throw up your hands and say "I don't remember, it's been a long time since I was in school"? Either you don't know C, or you don't know what a hash table is, which probably means you've never written anything in C but toy programs. (This is a toy program, of course, but many toy programs in C don't need hash tables.)
The compilable and fixed toy program in question is at http://canonical.org/~kragen/sw/dev3/dumbhash.c if you want to probe it for bugs.
It occurs to me that you might have been talking about something that isn't a C programming job. For example, we might be talking about a job programming an HTML database front-end in Python using Django. And of course it would be silly to reject someone for a job like that because they didn't know C, unless they claimed to know C on their résumé. And it's totally reasonable for a Python programmer, or a Python/Perl/JS/bash programmer, to not understand hash tables.
The K&R C Programming Language book has nearly the same example (although I recall it using linked-lists for hash collisions and not probing).
I would be mildly suspicious of anyone claiming to have a recent CS degree who wasn't familiar with the ideas.
I did. Works very well as an interview question.
> Most hash table code makes excessive use of malloc/realloc or has power of two growth
Libraries based on open addressing only call malloc when there is not enough room. That is not "excessive". Also, if you know the maximum size before hand, many libraries allow you to preallocate buckets such that you never trigger rehashing in this case.