Skip to content

Add swizzle_dyn_precise, mirroring swizzle_dyn from std::simd - #276

Open
Shnatsel wants to merge 23 commits into
linebender:mainfrom
Shnatsel:swizzle-dyn-precise
Open

Add swizzle_dyn_precise, mirroring swizzle_dyn from std::simd#276
Shnatsel wants to merge 23 commits into
linebender:mainfrom
Shnatsel:swizzle-dyn-precise

Conversation

@Shnatsel

@Shnatsel Shnatsel commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

I wondered how std::simd lowers swizzle_dyn - not the limited within_blocks version we added in #266 but the full arbitrary version - and the answer turned out to be "poorly". So I got nerd-sniped, badly. It's 4:30 AM as I write this.

The in-tree std::simd is trivially suboptimal, not even taking the most obvious optimization avenues like using 512-bit table lookups on NEON. The NEON gap is fixed upstream but not synced into std yet.

They also lower into a slow, branchy scalar fallback for all cases that don't have a native shuffle op available. There is a better way: if all you have is 128-bit vectors, you can get a 256-bit vector shuffle with four 128-bit shuffles and a couple bitwise ORs.

This PR fixes all of that:

  1. Implements all the native ops, and reuses the tuned AVX2 impl for 256-bit shuffles from std::simd (with attribution)
  2. Adds the "4 small shuffles make a big shuffle" decomposition for SSE4.2 and WASM
  3. Adds a further optimized 512-bit shuffle on AVX2 which is better than the generic decomposition

I've written a toy SIMD base64 decoder using 512-bit shuffles to benchmark this. On Zen4 the specialized AVX2 implementation improves the benchmark by 5%, but it'll never use it because it has AVX-512. On Zen2, where the specialized 512-bit AVX2 is supposed to help the most, there was no difference on benchmarks on real hardware at all, so I went ahead and deleted the specialized 512-bit AVX2 implementation.

Hand-written unit tests are included. Tests on random data that check the optimized SIMD impls against a trivial scalar impl were used to verify the implementation but aren't included in the final PR to avoid extra deps and slow tests (they're preserved in history for posterity but reverted).

I've implemented the *_precise variant first because it is genuinely important for some algorithms, mirrors the std::simd implementation exactly, and the "out of bounds is zero" property actually allows for cool optimizations for non-native vector widths.

This probably deserves a blog post.

@Shnatsel

Copy link
Copy Markdown
Contributor Author

I've also submitted similar changes upstream to std::simd:

One PR is already merged, the other is awaiting review.

@Shnatsel

Copy link
Copy Markdown
Contributor Author

The implementation has evolved somewhat since the PR description was written, here's a blog post that goes into detail and is more representative of the current state: https://shnatsel.github.io/improving-std-simd-swizzle-dyn/

Shnatsel added 14 commits July 23, 2026 08:56
…ecise and we can assert it has to always match the scalar fallback
… it's precise and we can assert it has to always match the scalar fallback" to avoid adding extra dependencies

This reverts commit 4e8a56a.
…eric split/combine since there's no difference on benchmarks on real Zen2 hardware, which was the best-case scenario for this implementation according to llvm-mca
… llvm-mca results: Haswell, Broadwell and Skylake effectively unchanged for the 256-bit case; Zen1, Tiger Lake show 23% improvement, Zen3 7% to 20% improvement. The 512-bit case that decomposes into this is about the same, but with a 20% improvement on Skylake and a clear 20% improvement on Zen3.
@Shnatsel
Shnatsel force-pushed the swizzle-dyn-precise branch from 0a551a7 to 8aee113 Compare July 23, 2026 08:32
Shnatsel added 3 commits July 23, 2026 13:51
# Conflicts:
#	fearless_simd_tests/tests/harness/lm_generated.rs
…e from vpcmpgtb + vpblendvb to vpaddb + vpor
@Shnatsel

Shnatsel commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

anxsan12 suggested a further optimization that turned out to be worthwhile, so I applied it.

It is also merged upstream: rust-lang/portable-simd#543

@Shnatsel
Shnatsel enabled auto-merge July 28, 2026 19:50
Doubles the throughput on both Ice Lake and Zen4.
Improves latency on Ice Lake from 23 to 13 cycles, on Zen4 from 17 to 15 cycles.

Improves encoding performance of a toy base64 implementation on Zen4 by -20% (time) +25% (throughput), no change on decoding benchmark
@Shnatsel

Shnatsel commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

@DJMcNab could you take a quick look?

All the complex parts (x86 implementations, 2x vector width trick) have already been reviewed upstream in std::simd:

WASM and NEON implementations are trivial.

There are unit tests and random testing to ensure correctness, so I'm quite confident in this code.

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.

1 participant