SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

Add runtime-dispatched SIMD xxHash3 via Highway (#31491)

## What

`Bun.hash.xxHash3` was backed by the `twox-hash` Rust crate, which
selects its SIMD backend **at compile time**. On the
`linux-x64-baseline` build (nehalem / SSE2) the long-input stripe loop
therefore never reaches AVX2, so it loses throughput relative to the
haswell build.

From the linux-x64 vs linux-x64-baseline canary comparison:
- `Bun.hash.xxHash3` 128 KB: 4.31 µs → 5.12 µs on baseline (**+19%**)

This PR **removes the `twox-hash` dependency entirely** and reimplements
all of xxHash (XXH3, XXH32, XXH64, and streaming XXH64) in C++. The XXH3
long-input stripe loop is a **runtime-dispatched Highway kernel**
(`HWY_DYNAMIC_DISPATCH`, the same mechanism as the existing
`highway_strings.cpp` kernels), so one binary picks the widest ISA the
CPU supports — regardless of the build's compile-time target.

## Benchmark

`bench/snippets/hash.mjs`, AVX-512 x86-64 host. Time per hash, **lower
is faster**; Δ is this branch vs 1.3.14.

**xxHash3 — 128 KB (the regression this PR targets):**

| Build | Bun 1.3.14 (Zig `std.hash`) | This branch | Δ |
|---|---|---|---|
| **baseline** (`linux-x64-baseline`) | 5,700 ns | **2,070 ns** |
**2.75× faster** |
| modern (`linux-x64`) | 3,120 ns | **2,080 ns** | **1.50× faster** |

The baseline row is the point: 1.3.14's baseline build is pinned to SSE2
at compile time, so even on an AVX-512 CPU it runs the 2-lane stripe
loop. This branch's baseline build ships every ISA variant and
dispatches to AVX-512 at runtime, so the same binary is **2.75× faster**
on that CPU — and the modern build is 1.5× faster too (Highway's AVX-512
kernel beats the Zig/LLVM auto-vectorized AVX2 loop).

**Other xxHash functions (modern build, 128 KB):**

| | Bun 1.3.14 | This branch |
|---|---|---|
| xxHash64 | 9,530 ns | 9,580 ns |
| xxHash32 | 19,050 ns | 19,090 ns |

XXH32/XXH64 have no SIMD form in the reference (they process 16/32-byte
stripes of scalar integer ops), so they're at parity — the C++ matches
the old codegen, there's no vector win to be had.

**Short input (8 bytes), modern build:** xxHash3 30.5 → 38.3 ns,
xxHash64 29.5 → 39 ns, xxHash32 24 → 38 ns. Short inputs are dominated
by a ~10 ns fixed cost that's uniform across all three (the signature of
call overhead, not compute): 1.3.14 called Zig `std.hash` inlined
in-process, whereas these now cross the Rust→C++ `extern "C"` boundary.
For `len ≤ 240` the kernel takes the scalar branches directly and never
touches `HWY_DYNAMIC_DISPATCH`, so the delta is purely the FFI crossing,
not dispatch or hashing.

## Cause

twox-hash's `xxhash3_64` compiles a single SIMD width chosen from the
build's target features. The baseline build has only SSE2, so the
64-byte stripe loop (`XXH3_accumulate_512` / `XXH3_scrambleAcc`) runs 2
lanes wide instead of 4 (AVX2) or 8 (AVX-512), even on CPUs that support
them.

## Fix

New Highway translation unit `src/jsc/bindings/xxhash3.cpp` holds the
whole of xxHash:

- Only `XXH3_accumulate_512` and `XXH3_scrambleAcc` — the hot stripe
loop for long inputs — are vectorized with Highway intrinsics and
dispatched at runtime. The stripe math is **per-64-bit-lane**, so scalar
/ SSE2 / AVX2 / AVX-512 all compute the same accumulators; that's what
makes xxHash3 safe to runtime-dispatch.
- The `0..240`-byte branches, the merge/avalanche finisher, and the
seeded custom-secret derivation are the reference's scalar code — they
don't depend on vector width.
- `HashLong` (the whole `len > 240` loop) is exported, so the ISA is
resolved **once per call**, not per stripe.
- XXH32, XXH64 (one-shot + streaming) are scalar C++ — no reference SIMD
form — compiled outside the per-ISA namespace, so no new
baseline-allowlist entries.
- Exposed as `extern "C"` (`highway_xxhash3_64` / `highway_xxhash32` /
`highway_xxhash64` + the streaming trio); `bun_highway` wraps them and
`bun_hash` / `HashObject` now forward there.

`twox-hash` is **removed from the workspace** (`Cargo.lock` no longer
contains it). Constants (`XXH3_kSecret`, all PRIMEs) are byte-identical
to the xxHash reference.

## Verification

Output is **bit-identical to the xxHash reference**
(`XXH3_64bits_withSeed` / `XXH32` / `XXH64`, v0.8.2 — same output as the
retired twox-hash and as Zig's `std.hash.XxHash*`):

- The `xxHash3 SIMD kernel` and `xxHash32 / xxHash64 reference vectors`
tests in `test/js/bun/util/hash.test.js` assert exact reference values
across every length branch (16 / 128 / 240 cutoffs, 64-byte stripes,
multi-block 64 KB / 128 KB) and seeds `{0, 42, 0xABCDEF01}`, and
cross-check the dispatched kernel against `Bun.hash.xxHash3`. This suite
runs in CI on every platform (20 tests, 136 `expect()` calls).
- Offline, the kernel was checked byte-for-byte against the reference
across **lengths 0..≈70 K and 7 seeds** forcing **each dispatch target**
— EMU128 (scalar), SSE2, SSSE3, SSE4, AVX2, AVX-512 — all identical.
This is what guarantees the `linux-x64-baseline` (SSE2) build produces
the same output as the haswell build.
- Bundler `[hash]` snapshots (streaming XXH64 via `ContentHasher`), the
dev-server source-map hash, and the resolver stat-hash tests are
unchanged and pass; `cargo miri test -p bun_hash` is green (the xxHash
FFI tests are gated out of Miri since it can't call `extern "C"`; the
other hashes stay covered).

The new TU is registered in `noUnify` (third Highway TU —
`foreach_target.h`'s TU-wide include guard needs it to expand its own
per-ISA namespaces, like `image_resize.cpp`). Per-ISA `HashLong` symbols
are added to the three `verify-baseline-static` allowlists (they're
reached only via `hwy::SupportedTargets` runtime dispatch).

## Notes / out of scope

The canary also flagged:
- `Bun.hash.cityHash64` (short input): 24.0 → 25.5 ns (+6%)
- `Bun.hash.xxHash32` 128 KB *faster* on nehalem (29.7 → 17.7 µs)

I left these out deliberately. CityHash64 on short (8-byte) input is
straight-line 64-bit integer mixing with no SIMD to dispatch — the +6%
is baseline codegen, not a missing-SIMD regression, so a Highway port
wouldn't address it. The xxHash32 inversion is a separate codegen issue
on the haswell build; porting xxHash32 to SIMD is a distinct change (the
reference has no SIMD XXH32). Happy to follow up on either if wanted.
R
robobun committed
63d5cd40655a225142a86a9d7798e4eae5948952
Parent: 29e38e9
Committed by GitHub <noreply@github.com> on 5/28/2026, 8:43:50 PM