Live data from Hacker News

Why Hashbrown Does a Double Lookup

gankro.github.io

81–90 of 116 posts

Re: Why Hashbrown Does a Double Lookup

#81
post #73
post #26

The author is also in the thread on /r/rust if you have any questions for them: https://www.reddit.com/r/rust/comments/b38cwz/why_hashbrown_...

I went to university with him. Really nice guy. Every time we talk, I say something like "any day now dude, I'm going to really get into Rust" and then I never do because I'm awful.

I've been saying the same since last year. I'm finally picking it up just trying to use it for regular everyday scripting work. It's been slow but atleast I'm starting now.

Re: Why Hashbrown Does a Double Lookup

#82
post #76

Earlier quoted context omitted.

You can eliminate tombstones over time by: # any time a tombstone immediately preceeds a empty, it can be marked empty [ $ _ ] -> [ _ _ ] # any time you lookup a key, it can be swapped with a tombstone immediately preceeding it [ $ A ] -> [ A $ ] # (moving the tombstone closer to a empty that will destroy it) # if you don't have iterators, you can also jump over other keys [ $ B A ] -> [ A B $ ] [ $ B C A ] -> [ A B…

Mutating the table on lookup seems pretty gross, though.

Eh, it’s the classic amortized approach. Whoever you ca “touch” the data and you’re right there already due to a lookup, it makes sense to amortize your data structure housekeeping IMO.

TBH, the right answer is always due to the users use case (Amortization and housekeeping really help with purely functional data structures), and benchmark data.

Re: Why Hashbrown Does a Double Lookup

#84
post #81
post #73

Earlier quoted context omitted.

I went to university with him. Really nice guy. Every time we talk, I say something like "any day now dude, I'm going to really get into Rust" and then I never do because I'm awful.

I've been saying the same since last year. I'm finally picking it up just trying to use it for regular everyday scripting work. It's been slow but atleast I'm starting now.

Slow indeed. Too slow...

Re: Why Hashbrown Does a Double Lookup

#85
post #56

Earlier quoted context omitted.

I assume that’s constant lookup time assuming well distributed hash values? The absolute worst case for a hash table is when every key has the same hash, and then you must fall back to something that’s at best log-n time.

The standard Java java.util.HashMap currently falls back to a Tree(Map) if a bucket gets too full (more than 5? Not sure, a LinkedList if not too full). But I wonder, couldn't you not use a different hash and have use a HashMap as a bucket?

My limit is 128. 5 is very common with a high load factor and fast hash function.

Re: Why Hashbrown Does a Double Lookup

#86
post #76

Earlier quoted context omitted.

You can eliminate tombstones over time by: # any time a tombstone immediately preceeds a empty, it can be marked empty [ $ _ ] -> [ _ _ ] # any time you lookup a key, it can be swapped with a tombstone immediately preceeding it [ $ A ] -> [ A $ ] # (moving the tombstone closer to a empty that will destroy it) # if you don't have iterators, you can also jump over other keys [ $ B A ] -> [ A B $ ] [ $ B C A ] -> [ A B…

Mutating the table on lookup seems pretty gross, though.

I would argue that "the table" is not mutated, only the internal state of its implementation. Every time you access any information, a cache at some layer below you is updated. Is that also gross?

Re: Why Hashbrown Does a Double Lookup

#87
post #83
post #2

This is pretty amazing, i really like the fact that a fastest hash table implementation is going to be part of the Rust's stdlib.

Where does it say that this is the fastest hash table implementation?

It's accepted knowledge that the Google Swisstable is currently the fastest hashtable around. (The C++ implementation) But there are three variants, and this looks like the slowest of the three, but the best for dynamic languages.

Re: Why Hashbrown Does a Double Lookup

#88
post #76

Earlier quoted context omitted.

You can eliminate tombstones over time by: # any time a tombstone immediately preceeds a empty, it can be marked empty [ $ _ ] -> [ _ _ ] # any time you lookup a key, it can be swapped with a tombstone immediately preceeding it [ $ A ] -> [ A $ ] # (moving the tombstone closer to a empty that will destroy it) # if you don't have iterators, you can also jump over other keys [ $ B A ] -> [ A B $ ] [ $ B C A ] -> [ A B…

Mutating the table on lookup seems pretty gross, though.

No, that's the common approach with chained lists: Move found list item to first.

Re: Why Hashbrown Does a Double Lookup

#89
>And so our "two loops" are usually just "do two simple SIMD operations". Not quite so offensive when you say it like that!

No, it's not quite so offensive, but this doesn't explain why it's the best option. Is there no equally-fast way to write the first-tombstone implementation with SIMD instructions? The answer seems to be in the sketch of the implementation, which I'm having trouble understanding.

EDIT: I'm watching the original SwissTable talk now... would it really have been worse to use 2 bits for empty/tombstone/full/sentinel, and 6 bits for hash prefix?

EDIT 2: More implementation info. Tombstones are actually rare, because if any element in your 16-wide chunk is empty, you don't have to create a tombstone. In the very best case (a perfectly distributed hash function), your hashmap has to be ~94% full before it's even possible to fail this. Because tombstones are so rare, it's better to save the single bit for extra confidence in the hash prefix.

So, here is my understanding of the implementation and its rationale:

* Every bucket in the backing array has a corresponding byte in a metadata array

* 1 bit of this byte stores whether the bucket is empty, the other 7 bits are for a hash prefix

* SIMD instructions search 16 bytes at a time, checking: this bucket is not empty, this bucket's key matches the first 7 bits of my key

* Since 16 buckets are checked for emptiness at the same time, you can avoid creating a tombstone for a bucket if any of the other 15 buckets are empty (just set it to empty, i.e. set the first bit to 0)

* This means that tombstones are very unlikely- you'll probably rehash before you get to the load factor where you start seeing tombstones

* Since tombstones are so unlikely, it's more valuable to add an extra bit to the hash prefix than it is to quickly find tombstones

My question remains: why can't the first search return the offset of the first empty bucket? In this loop, why is there not an else that saves the offset?: https://github.com/abseil/abseil-cpp/blob/256be563447a315f2a...

Re: Why Hashbrown Does a Double Lookup

#90
post #76

Earlier quoted context omitted.

You can eliminate tombstones over time by: # any time a tombstone immediately preceeds a empty, it can be marked empty [ $ _ ] -> [ _ _ ] # any time you lookup a key, it can be swapped with a tombstone immediately preceeding it [ $ A ] -> [ A $ ] # (moving the tombstone closer to a empty that will destroy it) # if you don't have iterators, you can also jump over other keys [ $ B A ] -> [ A B $ ] [ $ B C A ] -> [ A B…

Mutating the table on lookup seems pretty gross, though.

Well, it still works (just slower) if you only do fixups during {table[key] = val} operations. But honestly, if you're using a probabilistic data structure like a hash table, the ship has already sailed on gross implementation details.
Post reply on HN