An overengineered solution to `sort | uniq -c` with 25x throughput (hist) (github.com)
Was sitting around in meetings today and remembered an old shell script I had to count the number of unique lines in a file. Gave it a shot in rust and with a little bit of (over-engineering)™ I managed to get 25x throughput over the naive approach using coreutils as well as improve over some existing tools.
Some notes on the improvements:
1. using csv (serde) for writing leads to some big gains
2. arena allocation of incoming keys + storing references in the hashmap instead of storing owned values heavily reduced the number of allocations and improves cache efficiency (I'm guessing, I did not measure).
There are some regex functionalities and some table filtering built in as well.
happy hacking
48 comments
[ 0.23 ms ] story [ 65.2 ms ] threadThere is no text encoding processing, so this only works for single byte encodings. That probably speeds it up a little bit.
Depending on the size of the benchmark input, sort(1) may have done disk-based sorting. What's the size of the benchmark input?
It is not difficult to construct scenarios where throughput matters but that IMHO that does not determine engineering vs overengineering. What matters is whether there are requirements that need to be met. Debating the requirements is possible but doesn't take away from whether a solution obtained with reasonable effort meets the spec. Overengineering is about unreasonable effort, which could lead to overshoot the requirements, not about unreasonable requirements.
Could you explain that, if you have the time? Is that for writing the output lines? Is actual CSV functionality used? That crate says "Fast CSV parsing with support for serde", so I'm especially confused how that helps with writing.
[1] https://clickhouse.com/docs/operations/utilities/clickhouse-...
It likely won’t matter much here, but invoking cat is unnecessary.
will do the job just fine. GNU’s sort also has a few flags controlling buffer size and parallelism. Those may matter more (see https://www.gnu.org/software/coreutils/manual/html_node/sort...)You're right that the `cat` is unnecessary - and removing it actually had some marginal gains to the naive solution. I've updated the benchmarks to show this
Cheers
https://github.com/donatj/unic
It may not be the most fair comparison because with these random fastqs I'm generating the vast majority of the input is unique so it could be overloading the cuckoo filter.
Would be interesting to try this out
Are there any tools that tolerate slight mismatches across lines while combining them (e.g., a timestamp, or only one text word changing)?
I attempted this with a vector DB, but the embeddings calculation for millions of lines is prohibitive, especially on CPU.
https://blog.cloudflare.com/when-bloom-filters-dont-bloom/
https://github.com/majek/mmuniq
``` clang \ -g -ggdb -O3 \ -Wall -Wextra -Wpointer-arith \ -D_FORTIFY_SOURCE=2 -fPIE \ mmuniq.c \ -lm \ -Wl,-z,now -Wl,-z,relro \ -o mmuniq mmuniq.c:1:10: fatal error: 'byteswap.h' file not found 1 | #include <byteswap.h> | ^~~~~~~~~~~~ 1 error generated. make: ** [mmuniq] Error 1 ```
Arguably, this will result in a slower result in most cases, but the reason for the rejection is wasting developer time (not to mention time to test for correctness) to re-develop something that is already available in the OS.
This Rust implementation uses hashmap, if you have a lot of unique values, you will need a lot of RAM.
So in those settings I think it's absolutely worth it
perl -ne 'print if ! $a{$_}++'
Looking at the code, there are a few things I would consider optimizing. I'd start by trying (my) StringZilla for hashing and sorting.
HashBrown collections under the hood use aHash, which is an excellent hash function, but on both short and long inputs, on new CPUs, StringZilla seems faster [0]:
A similar story with sorting strings. Inner loops of arbitrary length string comparisons often dominate such workloads. Doing it in a more Radix-style fashion can 4x your performance [1]: Bear in mind that "compares/s" is a made-up metric here; in reality, I'm comparing from the duration.[0] https://github.com/ashvardanian/StringWars?tab=readme-ov-fil...
[1] https://github.com/ashvardanian/StringWars?tab=readme-ov-fil...
https://github.com/JoshRodd/mll
This is a strange benchmark [0] -- here is what this random FASTQ looks like:
There are going to be very few [*] repeated strings in this 100M line file, since each >seq.X will be unique and there are roughly a trillion random 4-letter (ACGT) strings of length 20. So this is really assessing the performance of how well a hashtable can deal with reallocating after being overloaded.I did not have enough RAM to run a 100M line benchmark, but the following simple `awk` command performed ~15x faster on a 10M line benchmark (using the same hyperfine setup) versus the naïve `sort | uniq -c`, which isn't bad for something that comes standard with every *nix system.
[0] https://github.com/noamteyssier/hist-rs/blob/main/justfile[*] Birthday problem math says about 250, for 50M strings sampled from a pool of ~1T.
There are definitely other benchmarks that we could try as well to test other characteristics as well.
I've actually just added in this `awk` implementation you provided to the benchmarks well.
Cheers!
Which could be totally useful in itself, but not even close to what "sort" is doing.
Did you run sort with a buffer size larger than the data? Your specialized one-pass program is likely faster, but at least the numbers would mean something.
That said, I don't see what is over-engineered here. It's pretty straightforward and easy to read.
But you're right - it will be limited by RAM in a way the unix tools are not.
I did add a test with sort using a larger buffer size to the benchmarks as well.
Somebody, implement `uniq --global` switch already. Put it into your resume, it's a legitimate thing to brag about.