Skip to content

Use packed char search on WASM via PackedSimd - #135896

Open
lewing wants to merge 4 commits into
dotnet:mainfrom
lewing:lewing-wasm-packed-span-helpers
Open

lewing wants to merge 4 commits into
dotnet:mainfrom
lewing:lewing-wasm-packed-span-helpers

Conversation

@lewing

@lewing lewing commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

PackedSpanHelpers narrows two Vector128<ushort> into one Vector128<byte> to search 16 chars per compare, but was gated on Sse2 only, so WebAssembly always took the non-packed path. PackedSimd.ConvertNarrowingSaturateUnsigned (i8x16.narrow_i16x8_u) has the same saturation semantics as Sse2.PackUnsignedSaturate, so the existing CanUsePackedIndexOf rules apply unchanged.

  • PackedIndexOfIsSupported becomes Sse2 || PackedSimd; PackSources/LoadPacked128 narrow with PackedSimd, using explicit IsSupported chains so ILLink removes dead branches.
  • string.Split and SingleStringSearchValuesPackedThreeChars use the packed path on wasm.
  • [CompExactlyDependsOn(typeof(PackedSimd))] lets CoreCLR wasm R2R precompile these methods.

This applies to Mono (interpreter and AOT) and CoreCLR R2R; the CoreCLR interpreter reports PackedSimd.IsSupported == false.

CoreCLR browser-wasm R2R, new/base len=16 len=1024
span.IndexOf(char) 1.07 0.60
span.IndexOfAny(c0, c1, c2) 1.17 0.71
SearchValues<string> 1.05 0.61

Short inputs cost about 1–2 ns per call on Arm hosts, where V8 and Cranelift lower the narrow and bitmask to multi-instruction sequences; on x64 they map to packuswb/pmovmskb (not measured).

Full benchmark table and Arm details

Apple M-series host under Node/V8, five interleaved rounds per build, medians; ratio = new / baseline.

Benchmark len=16 len=64 len=1024
span.IndexOf(char) 1.07 1.01 0.60
span.IndexOfAny(c0, c1) 1.10 1.02 0.70
span.IndexOfAny(c0, c1, c2) 1.17 1.01 0.71
span.IndexOfAnyExcept(char) 1.11 1.00 0.60
span.Contains(char) 1.04 1.11 0.63
string.IndexOf(char, OrdinalIgnoreCase) 1.05 0.96 0.64
SearchValues<char> (3 values) 1.13 1.05 0.73
SearchValues<string> (1 value) 1.05 1.04 0.61
string.Split (3 separators, dense) 1.07 1.06 1.03
string.Replace(string, string) (1-char old value, dense) 1.04 1.07 1.15

Mono browser-wasm shows the same shape: AOT 0.66–0.80 at len=1024 and 1.09–1.27 at len=16; the interpreter 0.65–0.85 at most sizes. Results break even around 64–96 chars.

On aarch64, V8 (d8 --print-wasm-code) and Cranelift lower i8x16.narrow_i16x8_u to a dependent sqxtun/sqxtun2 pair and i8x16.bitmask to a two-pmull + trn2 sequence; i16x8.bitmask needs one pmull. A possible follow-up keeps the packed any_true loop but computes the match index from the unpacked 16-bit vectors, as MakeSeparatorListVectorized does.

Related: #135869 (other CompExactlyDependsOn(PackedSimd) gaps), #135892 (crossgen2 doesn't fold box T; isinst X).

Note

This PR description was drafted with GitHub Copilot assistance.

lewing and others added 2 commits October 15, 2026 13:16
PackedSpanHelpers was gated on Sse2 only, so WASM fell back to the
non-packed Vector128 search. PackedSimd.ConvertNarrowingSaturateUnsigned
(i8x16.narrow_i16x8_u) has the same saturation semantics as
Sse2.PackUnsignedSaturate, so the same CanUsePackedIndexOf restrictions
apply.

- Enable PackedIndexOfIsSupported for PackedSimd and pack with
  ConvertNarrowingSaturateUnsigned.
- Add CompExactlyDependsOn(PackedSimd) to the packed helpers and the
  Any*CharPacked SearchValues so crossgen still compiles them for wasm.
- Use the packed path in string.Split's MakeSeparatorListVectorized.
- SingleStringSearchValuesPackedThreeChars is selected via
  PackedIndexOfIsSupported, so add a PackedSimd packing path there too
  (previously it would have fallen through to AdvSimd.Arm64.UnzipEven).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a42c1090-4687-4af7-92e0-1aa50cdd1826
Replace the nested ternary in SingleStringSearchValuesPackedThreeChars.LoadPacked128
and the two-way ternary in PackedSpanHelpers.PackSources with if/else-if chains that
recheck each IsSupported and end in ThrowUnreachableException, matching
TeddyHelper.LoadAndPack16AsciiChars. ILLink then removes the dead branches: the
nested ternary previously kept AdvSimd.Arm64.UnzipEven in trimmed wasm IL when
PackedSimd.IsSupported is not substituted (CoreCLR apps).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a42c1090-4687-4af7-92e0-1aa50cdd1826
@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-buffers
See info in area-owners.md if you want to be subscribed.

lewing and others added 2 commits October 16, 2026 15:28
All callers are already behind PackedIndexOfIsSupported, and ILLink folds a
two-way IsSupported ternary correctly, so the explicit if/else-if chain with an
unreachable throw added nothing here. Restore the Debug.Assert. The explicit
chain stays in LoadPacked128, where the nested three-way ternary left the dead
AdvSimd branch in trimmed CoreCLR wasm IL.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a42c1090-4687-4af7-92e0-1aa50cdd1826
Reverts d92832c. Sse2.IsSupported isn't substituted for CoreCLR apps targeting
x86/x64, so the two-way ternary left the dead PackedSimd call in trimmed x64 IL;
with the chain, PackedSimd.IsSupported (substituted false off wasm) lets ILLink
remove it. Matches the "explicitly recheck each IsSupported" pattern used
elsewhere in CoreLib.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a42c1090-4687-4af7-92e0-1aa50cdd1826

This branch has not been deployed

No deployments
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