Skip to content

chacha20: add WebAssembly SIMD (simd128) backend - #589

Merged
newpavlov merged 3 commits into
RustCrypto:masterfrom
TheUltDev:chacha20-simd128
Nov 6, 2026
Merged

newpavlov merged 3 commits into
RustCrypto:masterfrom
TheUltDev:chacha20-simd128

Conversation

@TheUltDev

@TheUltDev TheUltDev commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Adds a backend for WebAssembly's 128-bit SIMD, selected at compile time when the crate is built with the simd128 target feature (-C target-feature=+simd128), as the NEON backend is selected by target_feature = "neon" and as sha2 selects its wasm32_simd128 backend. Today wasm32 uses the software backend, a block at a time; simd128 is supported by every major WebAssembly engine (V8, SpiderMonkey, JavaScriptCore, Wasmtime, Wasmer).

The backend computes four blocks at a time with each vector holding one state word of the four blocks, so the rounds need no lane rotations (the layout of libsodium's u4); the blocks are transposed into rows once at the end. A single block (the tail of a message) is computed as its four rows, as the NEON backend does. The 16- and 8-bit rotations are byte shuffles (i8x16.shuffle), the 12- and 7-bit ones shift pairs. It supports both counter widths (Ietf's 32-bit and Legacy's 64-bit) and the RNG path, and chacha20_backend="soft" still forces the software backend. Only v128_store needs unsafe; the other intrinsics are safe with the target feature enabled.

Performance

The crate's own benches (cargo +nightly bench --target wasm32-wasip1 with RUSTFLAGS='-C target-feature=+simd128', the runner wasmtime run or Node 24's WASI) against cipher from master (ec30ea6), on an i9-14900K's performance cores, MB/s, the best of four runs; the software backend is the same build with --cfg chacha20_backend="soft":

soft, wasmtime simd128, wasmtime soft, Node 24 (V8) simd128, Node 24 (V8)
chacha20 16 B 842 941 842 888
chacha20 256 B 996 1,610 911 2,015
chacha20 1 KiB 1,011 1,646 899 2,106
chacha20 16 KiB 1,014 1,669 904 2,113
chacha12 16 KiB 1,588 2,658 1,620 3,343
chacha8 16 KiB 2,397 3,868 2,418 4,753

From four blocks on, 1.6 times the software backend's throughput under wasmtime and 2.3 times under V8; runs differ by about 4%. ChaCha20Rng::fill_bytes over 64 MiB: 1,006 → 1,538 MB/s under wasmtime, 942 → 2,086 under V8.

The 16 B case depends on cipher's wrapper: 0.5.2 enters the backend on every call, including the three of four that only read cached keystream, and zeroes a buffer of four blocks each time, which the software backend, with one parallel block, never does, so with 0.5.2 the 16 B case is 727 MB/s under wasmtime and 666 under V8 against the software backend's 800. RustCrypto/traits#2505 skips both, and with it a single block takes 62 ns straight from the core against the software backend's 70.

Testing

  • The crate's tests pass with +simd128 on wasm32-wasip1 for every feature combination (cargo hack test --feature-powerset under wasmtime), against cipher 0.5.2 and against cipher from master, and with chacha20_backend="soft" forced. The backend's output equals the software backend's over every length from 0 to 600 bytes as a stream, over 1 MiB + 77 bytes from nine seek positions, for XChaCha20, ChaCha8 and ChaCha12, and for ChaCha20Legacy and ChaCha20Rng across the 64-bit counter's carry at 2^32 blocks, on wasmtime, Wasmer 6.0.1 and Node 24.
  • cargo clippy --all-features --lib --tests -D warnings and cargo fmt --check are clean, and the crate builds on the MSRV (1.85) with +simd128.
  • Adds a CI job that runs cargo hack test --feature-powerset on wasm32-wasip1 under wasmtime, on the MSRV and stable, with and without simd128 (the matrix sha2's workflow runs).

@tarcieri

tarcieri commented Nov 1, 2026

Copy link
Copy Markdown
Member

Looks good at first glance. I can do a more detailed review later.

newpavlov pushed a commit to RustCrypto/traits that referenced this pull request Nov 2, 2026
`StreamCipherCoreWrapper::unchecked_apply_keystream_inout` and
`unchecked_write_keystream` enter the core's backend on every call, even
when the data has no whole block (a call that only consumes cached
keystream, or one shorter than a block), and `ApplyBlocksCtx::call`
zeroes a buffer of `ParBlocksSize` blocks before computing a tail that
may have zero blocks. A backend with parallel blocks (`chacha20`'s SSE2,
AVX2 and NEON backends, and the WebAssembly SIMD one of
RustCrypto/stream-ciphers#589) pays its setup and that zeroing on every
call, so small calls run slower with those backends than with the
software backend, whose `ParBlocksSize` is one.

This skips the backend when there are no whole blocks and the tail
buffer when the tail is empty. The keystream and the counter are
unchanged: the skipped calls computed nothing.
@tarcieri
tarcieri requested a review from newpavlov November 2, 2026 22:57

@newpavlov newpavlov left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a number of nits.

Also, please re-run the benchmarks with new version of cipher from the master branch. I probably will release it soon.

Comment thread chacha20/src/backends/simd128.rs Resolved
Comment thread chacha20/src/backends/simd128.rs Resolved
Comment thread chacha20/src/backends/simd128.rs Resolved
The counter row is incremented by one block in a method of the backend,
after each of the four blocks takes the state and after the single
block, instead of a free function adding a block count to the row. The
loop over the four blocks needs no index, so no `usize` to `u32` cast.

The crate's benches on wasm32-wasip1 under wasmtime and Node 24, with
`cipher` 0.5.2 and with `cipher` from master, are the same as before
within the run-to-run noise of about 4%.
@TheUltDev

Copy link
Copy Markdown
Contributor Author

Re-ran on cipher master (ec30ea6), description updated. 16 B is now faster than soft, nothing else moved.

@newpavlov
newpavlov merged commit 6ed73d6 into RustCrypto:master Nov 6, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants