Speeding up gearhash on ARM64
This post dives deep into the intricate process of optimizing the gearhash rolling hash algorithm for ARM64 architecture, achieving significant performance gains. It meticulously details the challenges of vectorizing a serial algorithm, such as dependency chains and compiler quirks, providing a masterclass in low-level performance tuning. The story resonates with Hacker News's appreciation for technical craftsmanship, system-level optimization, and the growing importance of ARM64 in modern computing.
The Lowdown
The gearhash crate, a Rust implementation of the GEAR rolling hash used in content-defined chunking for backup systems and large file storage, received a substantial performance upgrade for ARM64. Initially optimized for x86 (SSE4.2 and AVX2), the increasing prevalence of ARM64 platforms like Apple M-series chips and AWS Graviton processors highlighted the need for a NEON backend.
The optimization journey involved several key steps and insights:
- Initial Challenges: The gear hash's serial dependency chain (each byte's hash depends on the previous) and a gather-based table lookup made direct vectorization difficult.
- Parallelization Insight: The core breakthrough was realizing that the 64-bit hash shifts out its initial value after 64 bytes, allowing for parallel processing by splitting data into independent 'strips' and seeding each accordingly.
- Disappointing First Port: A direct port from SSE4.2 to NEON was slower than the scalar version (0.92x), as both had similar critical path latencies (around 2 cycles per byte), but the NEON version incurred additional overhead.
- Shortening the Dependency Chain: The author unrolled the hash calculation to combine two steps, reducing the critical path. However, compiler reassociation of additions on ARM64 caused an unexpected performance hit.
- The 'Naughty Fix': To prevent the compiler from reassociating sums and reintroducing critical path dependencies, table entries were combined in scalar registers before vector operations, making the vector operand opaque and resulting in a 1.46x speedup.
- Shifting Focus to Throughput: Further unrolling showed no additional speedup, indicating the process became throughput-bound. Optimization efforts then shifted to reducing instruction counts, particularly by improving load operations.
- Optimized Loads: Replaced multiple single-byte loads with a single unaligned 32-bit load, peeling off bytes with shifts, boosting performance to 1.63x.
- Optimized Boundary Checks: Consolidated four individual boundary tests into one vector operation, significantly reducing conditional branching in the common case and achieving a final 1.81x speedup.
The cumulative optimizations resulted in a 2.14x to 2.17x speedup for typical chunk sizes (64 KiB to 1 MiB) compared to the scalar version. While slower for very small chunks (below ~350 bytes), this update significantly enhances gearhash's performance on modern ARM64 systems, setting the stage for potential future re-optimizations on x86 platforms.