salsa20: add SSE2/AVX2 intrinsics backends#576
Conversation
|
We generally aim to minimize third-party dependencies. I would strongly prefer to have a proper self-contained intrinsics-based implementation instead. |
|
Yeah, we generally eschew crate dependencies for this sort of thing beyond our own ARX algorithms like Salsa/ChaCha in particular use a very limited number of operations (literally the three add-rotate-xor of ARX). Before looping in a crate dependency I'd be curious to see what is possible with auto-vectorization alone, just by using The |
|
Naive auto-vectorization performs worse because of register spills. I'll follow the the chacha20 example and do the diagonal layout trick. |
Add SIMD-accelerated Salsa20 backends using x86 intrinsics with runtime CPU detection via cpufeatures (no_std compatible, same pattern as chacha20). Both backends use diagonal SIMD layout: each register holds one diagonal of the 4x4 state matrix, with shuffles rotating between column and row round alignment. SSE2: 4 parallel blocks, 4 __m128i registers per block. AVX2: 4 parallel blocks via 2 __m256i sets, each packing 2 blocks in its 128-bit lanes. Dispatch order: AVX2 > SSE2 > scalar fallback. Measured 3.1x throughput on Salsa20/20 (543 -> 1673 MB/s) on x86_64 with AVX2.
|
PR updated. Auto-vectorization (with |
newpavlov
left a comment
There was a problem hiding this comment.
Could you generate Godbolt links to showcase generated assembly for SSE2 and AVX2 backends?
| if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { | ||
| let tokens = (avx2_cpuid::init(), sse2_cpuid::init()); | ||
| } else { | ||
| let tokens = (); |
There was a problem hiding this comment.
I think you can use let tokens = Default::default() here.
UPD: InitToken does not currently implement Default. We probably should amend it in a future release.
There was a problem hiding this comment.
Not possible because cpufeatures::InitToken doesn't impl Default. Instead, init_tokens() helper was extracted.
| state: [u32; STATE_WORDS], | ||
| /// CPU target feature tokens | ||
| #[allow(dead_code)] | ||
| tokens: Tokens, |
There was a problem hiding this comment.
Add reason to the allow attribute.
Remove `examples/ctbench.rs` and the `dudect-bencher` dev-dependency as requested during review. Criterion-based benchmarks will be added in the repo-level `benches/` crate instead.
- Add `reason` to `#[allow(dead_code)]` on the `tokens` field. - Extract `init_tokens()` helper to deduplicate the `cfg_if!` token initialization in `from_raw_state` and `KeyIvInit::new`. (`Default::default()` as suggested doesn't compile because `cpufeatures::InitToken` doesn't impl `Default`.) - Add PR reference `([RustCrypto#576])` to CHANGELOG entry.
Add `salsa20` throughput benchmark to the repo-level `benches/` crate, matching the existing `chacha20` benchmark structure.
|
FWIW we've used Scrutinizing the generated assembly will suffice for now. I've looked at other tooling for constant-time verification (e.g. |
|
Godbolt SSE2: https://rust.godbolt.org/z/Kao683b6v |
|
On quick glance (just the Godbolt links) the AVX2 version looks good. In the SSE2 version I'm seeing a massive block of (Sidebar: curious if Maybe we should just start with the AVX2 version since that seems more straightforward. Edit: actually the generated SSE assembly doesn't look that bad with const PAR_BLOCKS: usize = 2;Trying to do 4 blocks in parallel with SSE appears to be exceeding the available registers. |
|
So apparently PAR_BLOCKS=4 is still faster, despite the spills? It does seem smelly though. What should we do? It's a small perf gain, so I'd probably prefer fewer spills. That would also allow the compiler to use more registers for other stuff/avoid more reloads? I'm not too familiar with this stuff. I only learned about register spill like a month ago. |
|
Interesting. I dug up when we tuned So it seems to be faster in spite of the (egregious looking) stack spilling. |
|
What does the compiler do with unused registers before/after the call to salsa20? If salsa20 doesn't need them, then some of the application's registers can survive the call, right? So PAR_BLOCKS=2 => cleaner, slightly slower, but possibly faster for surrounding code (hot loops), but PAR_BLOCKS=4 => messy, faster salsa20, but maybe worse for hot loops? |
|
That's really going to depend on the code in question. I guess we could look specifically at crates like |
|
So I benched the SSE2 backend for Is it correct that the SSE2 backend would only be used on CPUs made in 2013 and older? |
|
For the most part, yes. On #379 I questioned whether it's worth supporting, but we ultimately continued to support it. |
|
Support sure, but what about the perf and register spill? PAR_BLOCKS=2 or 4? My digital helper just pointed out: On any machine old enough to lack AVX2, 12% better perf is probably worth having. Even if the assembly is ugly. |
|
Yes, that was the same argument in #379, which we ultimately went with. I can't speak to how it would operate on older hardware nor do I have access to such hardware to test with. It would probably be good for someone to test on actual hardware with only SSE2 that lacks AVX2. |
|
If you know someone with that kinda hardware? If not, I think we might be engaging in hypothetical/premature optimization... maybe the KISS route is: PAR_BLOCKS=2, clean assembly, and leave it be until there's someone complaining about salsa20 perf on ancient hardware. |
|
I mean, that's how I feel about the SSE2 backend as a whole, as I expressed in #379. It's hard to gauge the relative merits of a SIMD backend for hardware so old nobody has access to it to test on. |
|
So, kick it completely? I would not care. Slightly anachronistic because salsa20 is a bit older than chacha20. So keeping SSE2 at all is a hypothetical optimization. Nobody out there is doing perf-sensitive work on 13+ year old hardware. The crate would still work, just without the speedup. |
|
Yeah, I think we can start with AVX2 and then follow up on SSE2 and decide if the maintenance burden is worth it and if there are actually any users who can tell us how it should be tuned |
Per review feedback, start with AVX2 and follow up on SSE2 later once there is user demand to guide tuning.
cpufeatures__m128iregisters per block__m256isets, each packing 2 blocks in its 128-bit lanesno_stdcompatible, no new features requireddudectconstant-time timing tests (examples/ctbench.rs)