derive_hash_fast: Over 2x faster hashing than #[derive(Hash)]

151 points Shnatsel 24 comments

TL;DR: #[derive(Hash)] hashes struct fields one by one, which leaves performance on the table. derive_hash_fast crate hashes the entire struct at once, which is much faster. The difference for large slices is even more pronounced, with over 10x improvement.

Limitations: The struct must be safe to view as a slice of bytes. This is enforced by requiring derived traits from either bytemuck or zerocopy, at your option.

Tell me more

This crate is inspired by the excellent blog post by @purplesyringa (who is not affiliated with this crate). Check it out for an in-depth exploration of the issues with #[derive(Hash)] and the Hash trait in general.

We achieve better performance than #[derive(Hash)] by:

  1. Hashing the entire struct at once (as opposed to each field individually)
  2. Dispatching to a sequence of primitive writes such as hasher.write_u64 which is determined at compile time, padded where necessary (as opposed to using the slow variable-length codepath in the hashers)
  3. Replicating the optimization std performs for u8 and other primitive types in slices, so that e.g. &[MyType(u8)] can he hashed as fast as &[u8]. This applies to structs with multiple fields as well.

Usage

For using the crate with zerocopy (recommended), see the docs on derive_hash_fast_zerocopy!

For using the crate with bytemuck (which puts more restrictions on your type), see the docs on derive_hash_fast_bytemuck!

Benchmarks

Clone the repository and run cargo bench.

I've published the raw results from a run here, but nothing beats benchmarks on your hardware and on your verstion of Rust compiler. Or better yet, try it in a real project instead of relying on microbenchmarks!

FAQ

Please see the README for frequently asked questions.

Acknowledgements

Thanks to @purplesyringa for the detailed blog post that inspired this crate and to @Sawyer47 for the preliminary exploration and benchmarking.