Skip to content

Fill WASM SIMD gaps: Base64 vectorization and byte/ushort LeadingZeroCount - #131239

Merged
tannergooding merged 3 commits into
dotnet:mainfrom
tannergooding:tannergooding-wasm-simd-lightup-audit
Jul 23, 2026
Merged

Fill WASM SIMD gaps: Base64 vectorization and byte/ushort LeadingZeroCount#131239
tannergooding merged 3 commits into
dotnet:mainfrom
tannergooding:tannergooding-wasm-simd-lightup-audit

Conversation

@tannergooding

Copy link
Copy Markdown
Member

This is the first, low-risk slice of an audit of the vectorized libraries code (Vector<T>, Vector128/256/512<T>, System.Runtime.Intrinsics.*) for WebAssembly SIMD (PackedSimd) coverage. It fixes the genuine WASM gaps found so far and leaves the broader audit tracked for follow-ups.

The audit is grounded on the RyuJIT WASM backend, not Mono+LLVM. RyuJIT auto-lowers cross-platform Vector128/Vector<T> into PackedSimd during import/lowering, so xplat code is already WASM-accelerated. A genuine WASM gap therefore exists only where code uses raw Sse*/Avx*/AdvSimd intrinsics with no xplat fallback (so WASM drops to scalar), and the operation has a WASM equivalent.


Base64 encode/decode (Base64EncoderHelper.cs, Base64DecoderHelper.cs)

The Vector128 encode/decode loops were gated on Ssse3 || AdvSimd.Arm64 with no cross-platform fallback, so WASM fell all the way back to the scalar path. These are the only true "vector cliffs" the audit found. Both helpers are shared by Base64 and Base64Url, so this covers all four public surfaces.

The gate is widened to include PackedSimd, and the handful of x86/Arm-specific primitives are given PackedSimd branches:

  • SimdShufflePackedSimd.Swizzle (matches Ssse3.Shuffle's high-bit-clears-lane semantics via mask8F).
  • SubtractSaturatePackedSimd.SubtractSaturate.
  • The pmaddubsw/pmaddwd/MultiplyHigh reshuffles are emulated with per-lane logical shifts, exact for the 6-/10-/12-bit intermediates (e.g. pmaddubsw by {64,1,…} is (even_byte << 6) + odd_byte).

The emulations were validated bit-identical against the Sse2/Ssse3 intrinsics over random inputs. All new branches constant-fold away off-WASM (PackedSimd.IsSupported is a JIT constant), so there's no change to x86/Arm codegen.


byte/ushort LeadingZeroCount software fallback (TensorPrimitives.LeadingZeroCount.cs)

LeadingZeroCount previously vectorized only via a hardware vector clz — Avx512CD/Avx512Vbmi on x86, AdvSimd on Arm. WASM has no vector clz opcode, so it ran fully scalar; x86-without-AVX512 also ran scalar for byte/ushort.

This adds a table-free software fallback for byte/ushort only: smear the most-significant set bit down so every lower bit is set, then LeadingZeroCount == bitWidth - PopCount(smeared), reusing the existing PopCountOperator (the same idiom TrailingZeroCount already uses). Hardware paths keep priority; the fallback only fills the gap on platforms lacking a vector clz for these widths (WASM, x86-SSE).

Why byte/ushort only

The smear is a serial dependency chain of O(log bitWidth) instructions independent of lane count, so it only pays off with enough lanes to amortize the chain. Measured on an AVX2 box with Avx512CD.VL disabled to force the software path, comparing the actual TensorPrimitives.LeadingZeroCount<T> path before/after (best-of-N, warmed):

width lanes / V128 vec vs scalar
byte 16 3.27x
ushort 8 1.45x
uint 4 ~1.0x (break-even)
ulong 2 regresses

So uint/ulong keep their existing hardware-vector-clz gates and stay scalar where no vector clz exists — a single-instruction scalar clz (x86 lzcnt, wasm i32/i64.clz) beats a serial smear over only 2–4 lanes.

I also checked whether widening to Vector256 would flip uint/ulong to a win (more lanes amortizing the same chain, plus wider loads for memory-bound inputs). It doesn't hold up — uint only wins at the L1-resident (1.21x) and truly DRAM-bound (1.18x) extremes but loses ~2x across the L2/L3 middle where most inputs land; ulong never breaks even (0.54–0.75x). The scalar loop sustains higher memory-level parallelism than the long dependency chain, so the wide-vector version is left out.


Audit coverage (for reviewer context)

~68 vectorized files were reviewed. Breakdown:

  • Already WASM-explicit (24): carry a PackedSimd branch alongside x86/Arm (e.g. HexConverter, Guid, Matrix4x4, the ASCII/UTF8/Teddy/Probabilistic SearchValues).
  • Already xplat-covered (20): the internal ISA branch is an AVX2 wider-than-128 fast path or a hardware clz/round lightup; the Vector128/Vector<T> fallback is what RyuJIT lowers on WASM, so no cliff (e.g. SpanHelpers.Byte/Char, Ascii.*, Math/MathF, TrailingZeroCount, Latin1 narrowing).
  • Scalar by nature (13): CRC (carryless-multiply tables), FMA, and similar — no WASM vector equivalent, or inherently scalar.
  • Fixed here (4): the Base64/Base64Url helpers above.
  • Intentionally left as an x86-only optimization (7): the packed SearchValues family (SpanHelpers.Packed.cs + Any*CharPacked*). Its throughput win rests on a cheap movemask + saturating narrow. Those are cheap when a WASM engine runs on an x86 host but lower to the same expensive emulation on an Arm host — which is exactly why native Arm (AdvSimd) is deliberately excluded there. Since we can't condition on the host ISA from within WASM, enabling it risks the Arm worst-case regression, so PackedIndexOfIsSupported stays x86-only. WASM already uses the non-packed (still vector-accelerated) searcher.

Note

This PR description and the code changes were authored with GitHub Copilot.

tannergooding and others added 3 commits July 22, 2026 14:59
The Vector128 encode loop was gated on Ssse3 or AdvSimd.Arm64 with no cross-platform fallback, so WASM fell back to the scalar EncodeThreeAndWrite path. Widen the gate to PackedSimd and add the corresponding branches: SimdShuffle uses PackedSimd.Swizzle (matching Ssse3.Shuffle's high-bit-clears-lane semantics via mask8F), SubtractSaturate uses PackedSimd.SubtractSaturate, and the Sse2.MultiplyHigh reshuffle is emulated with per-lane logical shifts (multiplying the u16 lanes by 2^6/2^10 is a right shift by 10/6). The remaining ops are already cross-platform Vector128 APIs the JIT lowers on WASM. New branches constant-fold away off-WASM.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Vector128 decode loop was gated on Ssse3 or AdvSimd.Arm64 with no cross-platform fallback, so WASM fell back to the scalar path. Widen the gate to PackedSimd and emulate the two MultiplyAddAdjacent merges: pmaddubsw by {64,1,...} becomes (even_byte << 6) + odd_byte and pmaddwd by {4096,1,...} becomes (even_i16 << 12) + odd_i16, both exact for the 6-bit/12-bit intermediate values. SimdShuffle already handles PackedSimd, and TryLoadVector128/TryDecode128Core use cross-platform Vector128 APIs. Emulations validated bit-identical against the Sse2/Ssse3 intrinsics over random inputs; branches constant-fold away off-WASM.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Smear the most-significant set bit down and compute bitWidth - PopCount,
reusing PopCountOperator. Only byte/ushort are vectorized this way; the
serial smear chain over the 2-4 lanes of uint/ulong doesn't beat a
single-instruction scalar clz (x86 lzcnt, wasm i32/i64.clz), so those
keep their existing hardware-vector-clz gates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-numerics
See info in area-owners.md if you want to be subscribed.

@tannergooding

Copy link
Copy Markdown
Member Author

CC. @dotnet/wasm-contrib. This should be the only vector paths that didn't have WASM lightup.

The search was oriented around RyuJIT WASM, but the necessary intrinsics should all be accelerated for Mono WASM as well (CC. @lewing)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR expands WebAssembly SIMD (PackedSimd) coverage for existing vectorized library implementations by adding PackedSimd-gated branches where code previously only accelerated on x86/Arm-specific intrinsics, and by introducing a software-vector fallback for byte/ushort LeadingZeroCount in TensorPrimitives.

Changes:

  • Enable the Vector128 Base64 encode/decode fast paths when PackedSimd.IsSupported, and provide PackedSimd equivalents / emulations for the small set of ISA-specific primitives used in those loops.
  • Add a table-free software-vector fallback for TensorPrimitives.LeadingZeroCount<byte/ushort> using bit-smear + vector PopCount, while keeping hardware vector-CLZ paths for wider element sizes.
Show a summary per file
File Description
src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64EncoderHelper.cs Adds PackedSimd to the Vector128 encode gate and implements PackedSimd-specific equivalents for operations previously only available via x86/Arm intrinsics.
src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Base64Helper/Base64DecoderHelper.cs Adds PackedSimd to the Vector128 decode gate and implements a PackedSimd Swizzle-based shuffle to match SSSE3/AdvSimd semantics.
src/libraries/System.Numerics.Tensors/src/System/Numerics/Tensors/netcore/TensorPrimitives.LeadingZeroCount.cs Adds a software-vector fallback for byte/ushort leading-zero count using smear + PopCount, preserving hardware-vector-CLZ-only behavior for uint/ulong.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 0

@tannergooding

Copy link
Copy Markdown
Member Author

There's notably some opportunities to reuse existing xplat vector APIs, but I'm not sure Mono handles them all today and so I went with the platform specific paths instead. We can clean up in .NET 12

@lewing

lewing commented Jul 22, 2026

Copy link
Copy Markdown
Member

There's notably some opportunities to reuse existing xplat vector APIs, but I'm not sure Mono handles them all today and so I went with the platform specific paths instead. We can clean up in .NET 12

The main arch we care about for mono on 11 is wasm and I'm willing to fix any issues there if you let me know what apis you're looking at.

@tannergooding

Copy link
Copy Markdown
Member Author

The main arch we care about for mono on 11 is wasm and I'm willing to fix any issues there if you let me know what apis you're looking at.

I'll take a deeper look and get back to you. It's mostly just about simplification of the managed code at this point, not accelerating it more, so I viewed it as a lower priority than this PR (which explicitly accelerates).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants