3 ms·
From my measurements, your optimized version runs at about the same speed as mine: $ cd /tmp/ $ git clone -b ag/test-kimono https://github.com/BurntSus
by burntsushi 4y ago
From my measurements, your optimized version runs at about the same speed as mine:
$ cd /tmp/
$ git clone -b ag/test-kimono https://github.com/BurntSushi/countwords
$ cd countwords/rust/
$ ./bench
<snip>
Summary
'./optimized-trie/target/release/countwords < kjvbible_x10.txt' ran
1.22 ± 0.02 times faster than './kimono-simple/target/release/countwords < kjvbible_x10.txt'
1.26 ± 0.02 times faster than './optimized-customhashmap/target/release/countwords < kjvbible_x10.txt'
1.56 ± 0.03 times faster than './optimized-unsafe/target/release/countwords < kjvbible_x10.txt'
1.58 ± 0.03 times faster than './kimono-optimized/target/release/countwords < kjvbible_x10.txt'
1.60 ± 0.03 times faster than './optimized/target/release/countwords < kjvbible_x10.txt'
3.97 ± 0.06 times faster than './simple/target/release/countwords < kjvbible_x10.txt'
7.58 ± 0.21 times faster than './bonus/target/release/countwords < kjvbible_x10.txt'
You mentioned in another comment that you were benchmarking on an M1. Maybe there's some interesting differences there in the codegen, how the CPU executes or both.
Your 'fast-simple' version is a different story though. Personally, I would not put that in the "simple" classification as outlined by the OP. It is IMO very much not the first program someone would write for this. Bringing in 'hashbrown' and futzing with the hash table lookup/insert is definitely a perf tweak that you probably wouldn't want to do unless you had to, because it makes the code more complex. The 'Box<[u8]>' cements it as an 'optimized' variant.
Now it does look like a simpler variant of the "optimized" Rust program I wrote. I'm pretty impressed. I don't think I ever would have broken out of my local optima to discover that program. In particular, your approach appears to do 3 (and some change) passes over each buffer: 1) read up to '\n' (just the first line), 2) UTF-8 validation, 3) make lowercase and 4) split on whitespace. I realize chunking all of that up confers a lot of benefits, but still, 4 passes and it still being faster than 1 pass is very interesting.
I've glanced at the profiles of each program but haven't been able to solidly conclude anything more precisely about where exactly the benefit is. At this point, my next step would be to slowly translate my version into yours, and benchmark each step of the way until I could isolate the key change (assuming it is one change). But alas, I have run out of time this evening. :-)
Kudos!
(The other interesting thing to note here is that my 'trie' variant is now the fastest Rust submission. Previously, it was slower than the optimized variants on my older CPU. That's pretty neat.)
- mustache_kimono 4y ago> Kudos! Thanks! > I don't think I ever would have broken out of my local optima to discover that program. It really is the answer to what is the most knuckle-headed thing one could try, but I was curious what Rust-with-the-guardrails could do. > My 'trie' variant is now the fastest Rust submission Cool. Very interested why this is the case.
- burntsushi 4y agoThe "why" behind the trie is explained in the comments in the code. :-) The core of my hypothesis was to avoid hashing and hash table lookups at all. The problem with the trie, though, is that it does a memory access per byte. Whether and when this trade is beneficial is not totally clear to me, but clearly, it can vary. Whether my hypothesis is actually correct is also not something I'm certain of. Verifying this would take some time with: 1) looking at the codegen and 2) some use of `perf` to extract CPU counters for things like branch and cache misses, and see if a correlation can be established.
- mustache_kimono 4y ago> In particular, your approach appears to do 3 (and some change) passes over each buffer: 1) read up to '\n' (just the first line), 2) UTF-8 validation, 3) make lowercase and 4) split on whitespace. I really should have waited until I had more time to respond, but it really only should be 2, 3, and 4. I tried unsafe/unchecked for UTF8, and, yes, it is a modest bump, but I wanted to do it without unsafe. And 3 and 4 are really pretty fast for what they are. They both work on bytes and the str as_bytes transmute is virtually cost free from what I can tell.
- mustache_kimono 4y ago> It is IMO very much not the first program someone would write for this. True. > Bringing in 'hashbrown' and futzing with the hash table lookup/insert is definitely a perf tweak that you probably wouldn't want to do unless you had to, because it makes the code more complex. The 'Box<[u8]>' cements it as an 'optimized' variant. I would say, both these items, I added much later, and contribute substantially less to the bottom line performance than you might think. Bulk of the performance is elsewhere.