Designing a fast hash table
ilikebigbits.com
ilikebigbits.com
See more here: http://www.sebastiansylvan.com/post/robin-hood-hashing-shoul...
Jeff Preshing has written many interesting blog posts about the subject. For example, see his blog post on Leapfrog Probing( http://preshing.com/20160314/leapfrog-probing/ )
I'd heard that point about using a mask instead of modulo to fit the hash into the table range before, but never paid much attention to it. I figured with all of the other work going on in a hash table, the difference between one of two arithmetic operations would be negligible.
Right now, I'm working on a book on programming language interpreters. That includes implementing a hash table from scratch. I used "%" because I felt it made the behavior easier for readers to understand.
After I got it all up and running, I discovered that in some microbenchmarks were spending something like 10% of the total execution time on that "%". I changed it to use a mask and "&" and it made a dramatic performance difference.
So I think I'll be explaining both approaches in the book. The former for clarity and the latter as an example of an optimization.
It's well known that std::unordered_map is slow (see panic's comment). Some more worthy opponents would be Google's dense_hash_map [1] or sparsepp [2]
- Which CPU was used? Which compiler optimization flags? (2 million inserts per seconds seems slow, for a std::unordered_map, using 64 bit data)
- How does HashSet read operations perform vs std::unordered_map?
- How does external fragmentation handling in HashSet compares to std::unordered_map? (e.g. writing and deleting random keys, continuously)
For example, I read here (https://cr.yp.to/critbit.html) that critbit tries could be a better base structure for a language (ie: like dict and list are for python).
So, if we start today, what hash to use? What will be a better default structure (my understanding is that hash-maps is/was the way to go)?
First, it seems like the prefix-free requirement means that the strings can't contain internal NUL bytes. In Python you can store 'a\0' as well as 'a\0b' in a hash table -- how do you handle this with crit-bit trees?
Second, in Python, unlike JavaScript, arbitrary types can be hashable. Tuples are hashable. How do you store ('a', 0) and ('b', 1) as keys in a crit-bit tree?
Hashing keys is also easy: add a pointer to a linked list of key-value pairs to each leaf node. To save space you could even use a flag bit and then a pointer to a single key-value pair, or a linked list in case of collision.
Note that you can store tuples directly as the concatenation of their fields; no need to use hashing.
That's a very naive way to map using the delimeter of the tuple parts. You might have interior elements that are 10k long.
After a lot of perf testing, and that library along with others had the same issue with not allowing null bytes.
Initially i thought it was a deal breaker, especially since I needed to preserve order.
Ended up doing a possibly mutating prepass on any keys keys that might contain nulls (Which is generally very fast O(len(key)) before storing, to rewrite without them.
It was still worth it.
There just seems a lot wrong with the presentation of the benchmark comparisons...
Specifically where is a full suite of comparisons as compared with other libraries for time and space, insert and retrieval. Hell, I make those graphs for other people's code, let alone my own.
Usually when you come up with a new algorithm it is typical to be so over the moon to show how it compares all around. I'm not seeing this here and that is a red flag.
I'll definitely give it a whirl though as it sounds promising, although I'm personally more interested in order preserving hashes.
"They allow you to insert, erase and look up things in amortized O(1) operations (O(√N) time)." Are they staying that the average time is (O(√N) and that is amortized to ~ O(1)?
I assume (O(√N) is the same as .5 multiplied by N? Is that correct? I don't think I've seen a square root in Big O notation before.
I say "claim" because everything can be made to look like a fit on a log-log-plot. It's just memory hierarchy. (TLB misses do incur a logarithmic delay, because the page table usually uses a tree data structure, albeit with a high branching factor).
O(√N) means there will be at most C*√N operations (where C is some constant factor > 0)
"That I use Big O to analyze time and not operations is important."
This strikes me as an odd statement though as Big O is generally meant to describe a run time or space not operations. What am I missing?
When he says he's using big O for time and not operations, he's saying that n is time (as opposed to operations)
Some models where such things are studied in more details include the external memory model (which has a two-level hierarchy - cache and main memory, or main memory and disk) and cache-oblivious models.
Most of the time, you don't need that extra information, though, and the RAM model suffices.