Live data from Hacker News

Trees, Hash Tables and Tries

benlynn.blogspot.com

11–20 of 24 posts

Re: Trees, Hash Tables and Tries

#11

In the real world, it's unlikely that a trie of any real size will be comparably performant to a hash table with a sane hash function: trie algorithms involve moving from node to node, each of which will require an unpredictable retrieval from non-sequential memory. Memory latency is pretty high these days (~200 cycles), so hash tables will almost always be quicker. This is not to say that radix tries aren't extremel…

I've done a bit of work with tries and hash functions and found that in-memory tries are almost universally faster than in-memory hashes, no matter how good the hash function is. The difference though is on average around 20-30% faster if both implementations are well optimized. But they come with lots of downsides that need to managed well, tries explode into memory very quickly requiring all kinds of memory optimiz…

I've done a bit of work with tries and hash functions and found that in-memory tries are almost universally faster than in-memory hashes, no matter how good the hash function is.

Do you have any insight on why this would be? It's not very intuitive to me.

A hash-table with low load has relatively small hash buckets and so touches fairly little memory, and string keys are fairly local, so I would have expected memory costs to dominate. Insertion and deletion from a hash table should require fewer allocations than insertion and deletion from a trie.

Re: Trees, Hash Tables and Tries

#12

In the real world, it's unlikely that a trie of any real size will be comparably performant to a hash table with a sane hash function: trie algorithms involve moving from node to node, each of which will require an unpredictable retrieval from non-sequential memory. Memory latency is pretty high these days (~200 cycles), so hash tables will almost always be quicker. This is not to say that radix tries aren't extremel…

If I do recall correctly if you do not require dynamic insertion/removal; That is, you can make do with a one time construction. You will not need to traverse pointers but can put the trie into one contiguous block of memory.

Re: Trees, Hash Tables and Tries

#13
post #6
post #3

I will argue that a properly implemented tries are more compact but not necessarily faster than hash table. The fact is, there are some badly implemented tries which even worse than hash table in term of memory consumption. Also, I'd like to point out the actually trie implementation may not have a fix k child node. I most often use k = 256 or 512 for the first few layers and 4~8 for the last few one in order to get…

I will argue that a properly implemented tries are more compact but not necessarily faster than hash table. Where do you argue this? The fact is, there are some badly implemented tries which even worse than hash table in term of memory consumption. Any data structure can be badly implemented. What use is comparing a badly implemented data structure to a well-implemented one?

It seems in the article the author suggests that in certain case tries is faster than hash table.

Yes, you are right about every data structure can be badly implemented. But some data structures are more easily to get wrong. For example, everyone have no problem with selection sort, it is just hard to get it wrong. But for binary search, some decent programmer may write (a + b) / 2. Same case for tries, people more commonly (intuitively) write things like node_t* child[26];

Re: Trees, Hash Tables and Tries

#14

In the real world, it's unlikely that a trie of any real size will be comparably performant to a hash table with a sane hash function: trie algorithms involve moving from node to node, each of which will require an unpredictable retrieval from non-sequential memory. Memory latency is pretty high these days (~200 cycles), so hash tables will almost always be quicker. This is not to say that radix tries aren't extremel…

what about judy-arrays[http://judy.sourceforge.net/] ? they seem to be pretty seriously cache-optimized...

Re: Trees, Hash Tables and Tries

#15
In the same way, any naively "constant space" algorithm that indexes into an array with a fixed number of pointers or indices is technically O(log n) in space, since you need log n bits to index n objects. So a space factor of log n can generally be treated as constant in practice.

Re: Trees, Hash Tables and Tries

#16
post #12

In the real world, it's unlikely that a trie of any real size will be comparably performant to a hash table with a sane hash function: trie algorithms involve moving from node to node, each of which will require an unpredictable retrieval from non-sequential memory. Memory latency is pretty high these days (~200 cycles), so hash tables will almost always be quicker. This is not to say that radix tries aren't extremel…

If I do recall correctly if you do not require dynamic insertion/removal; That is, you can make do with a one time construction. You will not need to traverse pointers but can put the trie into one contiguous block of memory.

That is absolutely true - it's been done with B+Trees, for example, as CSS-Trees. This does make finds faster (because you eliminate the space in the cache that pointers would require), but you're still going to be doing a lot of nonsequential access when performing finds.

Re: Trees, Hash Tables and Tries

#17

In the real world, it's unlikely that a trie of any real size will be comparably performant to a hash table with a sane hash function: trie algorithms involve moving from node to node, each of which will require an unpredictable retrieval from non-sequential memory. Memory latency is pretty high these days (~200 cycles), so hash tables will almost always be quicker. This is not to say that radix tries aren't extremel…

I've done a bit of work with tries and hash functions and found that in-memory tries are almost universally faster than in-memory hashes, no matter how good the hash function is. The difference though is on average around 20-30% faster if both implementations are well optimized. But they come with lots of downsides that need to managed well, tries explode into memory very quickly requiring all kinds of memory optimiz…

That's really interesting - but it's hard for me to understand. What makes them faster for lookup than a hash table with a really good, fast hash function (say, murmurhash)? Does it depend on factors like string length? Basically, what's the environment?

My assumption is that it's faster because you might end up performing fewer string comparisons with the trie - if you have strings with common prefixes/sizes, this could cost you time on a hash with collisions. Although it seems like unless you have a lot of strings with common prefixes/sizes, this still ought to be pretty fast. And if you do have a lot of similar strings, you could store the hash value as well to speed this up.

My work on cache-sensitive data structures is in the integer space, so perhaps my ignorance when it comes to working with strings is showing :-).

Re: Trees, Hash Tables and Tries

#18

Earlier quoted context omitted.

I've done a bit of work with tries and hash functions and found that in-memory tries are almost universally faster than in-memory hashes, no matter how good the hash function is. The difference though is on average around 20-30% faster if both implementations are well optimized. But they come with lots of downsides that need to managed well, tries explode into memory very quickly requiring all kinds of memory optimiz…

That's really interesting - but it's hard for me to understand. What makes them faster for lookup than a hash table with a really good, fast hash function (say, murmurhash)? Does it depend on factors like string length? Basically, what's the environment? My assumption is that it's faster because you might end up performing fewer string comparisons with the trie - if you have strings with common prefixes/sizes, this c…

Bingo, string comparisons will eat you alive. Even if you have a very small percentage of collisions in your hash (even close to 0%), you still have to check if you have a collision to determine if you need to iterate to the next slot in the hash no matter what. If you are hashing strings, you have to compare the entire string, each character comparison is guaranteed to be at least one integer sub operation for 1 byte wide character sets and 2 for unicode.

But basically, tries compute almost as fast as just reading the string, while hashes have to read the string a few times AND compute the function AND deal with collisions (AND resize the hash which is stupid expensive).

So to either insert or lookup in a hash you have to:

1) Compute the hash from the string, which naively involves reading the entire string into memory and computing the hash code. So for each character you have to move at least one or two bytes into memory, do something with it (likely some kind of integer op), store the result, then move the next byte or two in, do an operation with that and so on. Let's not forget that I also have to check for end of string, which uses at least one sub operation for each character as well for C style strings.

Otherwise you keep a strlen counter in a register somewhere and do sub operations against that, either way.

1b)If you are smart you simply restrict the length you compute off of to some smaller length that will still do a pretty good job of assuring a reasonable level of uniqueness in the hash, but you can't ever guarantee it (uniqueness is the principle problem of hashing functions).

2) But still, let's assume I'm dealing with a nice small 16 character string (n=16). I have to compute many more operations than just the string length, at least 1 mov, 1 sub (to check for EOS), and then a few operations for your hash and probably some copies back into memory, so let's be generous and say 8 ops for each character. So if n=16 at least 128 ops to simply compute the hash.

3) Then we have to do some pointer arithmetic or some table lookups or whatever to get to the memory location and store the string. So in all we're talking about 500-1000 ops to insert a 16 character string without even checking for collisions.

4) Factor that in, assume no collision, we might be on the order of 1200 executed ops in the end. If we have a collision, a couple thousand or more.

So for n=16, ops=~1200 minimum.

5) Even if there isn't a collision, I have to do a linear string compare to the hash->string value in the hash to assure me that there wasn't a collision. So we have to start moving integers into registers and doing a bunch of subs again (and checking for EOS on both strings now). This could easily introduce another 50-100 ops to ensure no collision.

Lookups on the hash are also of a similar scale. Heaven help you if you have to expand the hash and move all that crap from one place to another.

For a trie, there is no such thing as collisions. In the naive version, you simply allocate an array in memory the length of the character set + 1 in memory, which takes a couple of ops since you know the character set + 1 length ahead of time, use the character (I'm assuming 1 byte character lengths, but the principle is the same for 2 byte) as an operand in some simple pointer arithmetic. And store a memory location pointer in each array index to the next array (of character set + 1) until you finish off the string. If you are inserting, point to one more array and set that extra + 1 byte to something meaningful. If you are doing a lookup, you check that location to see if the byte is set to something meaningful (or you can just set that on the last character instead of one more down the tree, whatever).

The point is, for a 16 character string you count say two ops for memory allocation, an op to find the slot in the array, and another op for allocation or value setting for each character.

So in all, an insert or lookup happens almost as fast as a direct string compare, maybe on the order of n=16, ops=have to short circuit the lookup (read: stop reading the string) as soon as you can't find just one character).

I'm wagging a bit, I've never built either structure in asm, but if you analyze the number of operations each takes, the trie is insanely faster. Even with memory access lags.

You can do some obvious speedups, like loading several characters into a register at a time for integer arithmetic, or loading the hash locations value chains or something...

One of the confusing points on this is that the big-O estimates I've seen never bother to account for the string reads and comparisons. With a hash you basically have to do them a minimum of two times, with a trie, only once (and sometimes not even that much). Usually you see hashes as O(1) best and O(n) worst, but really they are O(k) and O(k*n) where k is the string length while tries are really O(k).

All that being said, as soon as you try to work with either structure on disk, hashes easily overtake tries in terms of speed since hard drive random access times are much worse and tries require lots and lots and lots of random access (for each character).

http://www.statemaster.com/encyclopedia/Trie

Re: Trees, Hash Tables and Tries

#19
post #11

Earlier quoted context omitted.

I've done a bit of work with tries and hash functions and found that in-memory tries are almost universally faster than in-memory hashes, no matter how good the hash function is. The difference though is on average around 20-30% faster if both implementations are well optimized. But they come with lots of downsides that need to managed well, tries explode into memory very quickly requiring all kinds of memory optimiz…

I've done a bit of work with tries and hash functions and found that in-memory tries are almost universally faster than in-memory hashes, no matter how good the hash function is. Do you have any insight on why this would be? It's not very intuitive to me. A hash-table with low load has relatively small hash buckets and so touches fairly little memory, and string keys are fairly local, so I would have expected memory…

see http://news.ycombinator.com/item?id=1240914

Re: Trees, Hash Tables and Tries

#20

Earlier quoted context omitted.

That's really interesting - but it's hard for me to understand. What makes them faster for lookup than a hash table with a really good, fast hash function (say, murmurhash)? Does it depend on factors like string length? Basically, what's the environment? My assumption is that it's faster because you might end up performing fewer string comparisons with the trie - if you have strings with common prefixes/sizes, this c…

Bingo, string comparisons will eat you alive. Even if you have a very small percentage of collisions in your hash (even close to 0%), you still have to check if you have a collision to determine if you need to iterate to the next slot in the hash no matter what. If you are hashing strings, you have to compare the entire string, each character comparison is guaranteed to be at least one integer sub operation for 1 byt…

Fascinating - thanks for the detailed response, and that does make a lot of sense.

It seems that if string comparisons are the major cost, you could speed up collision handling on the hash table quite a bit by storing additional metadata. Storing the size of the string, for example, would allow you to skip a lot of comparisons - you can do a simple size comparison prior to actually comparing characters. This would also allow you to do slightly more funky string comparisons, such as comparing characters from the end first, or even starting at the middle, to skip common prefixes.

Alternatively, instead of storing the size you could store the computed hash value for each string, which would allow you to compare hash values rather than full strings. The likelihood of a collision on a 32 bit hash value (say) is pretty low, so you'd mostly be avoiding extraneous string comparisons. This would also speed up growing/shrinking the table quite a bit. Have you tried these sorts of changes, and what (if any) difference do they make?

I can see that these optimisations merely reduce the cost of collisions rather than the baseline non-collision cost, and it definitely makes sense that tries require fewer operations. What I wonder is whether, as you scale up the trie to very large datasets, the superiority of the trie tails off due to the memory latency? Or are tries basically always superior in memory, due to their depth never getting all that large?

(To be clear, I don't mean to doubt your work on this subject, since you're clearly rather more knowledgeable than I am - I'm just interested :-) )

Post reply on HN