From 4c87565ffe387262ccf8832b7686ff446f3cd464 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 15 Oct 2026 13:16:14 -0500 Subject: [PATCH 1/4] Use packed char search on WASM via PackedSimd 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 --- .../Any1CharPackedIgnoreCaseSearchValues.cs | 3 ++ .../Any1CharPackedSearchValues.cs | 3 ++ .../Any2CharPackedIgnoreCaseSearchValues.cs | 2 ++ .../Any2CharPackedSearchValues.cs | 3 ++ .../Any3CharPackedSearchValues.cs | 3 ++ ...ingleStringSearchValuesPackedThreeChars.cs | 15 ++++++--- .../Strings/StringSearchValues.cs | 4 +-- .../src/System/SpanHelpers.Packed.cs | 31 ++++++++++++++++--- .../src/System/String.Manipulation.cs | 2 +- 9 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedIgnoreCaseSearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedIgnoreCaseSearchValues.cs index dfe6b3631f709e..a5805bacead6f8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedIgnoreCaseSearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedIgnoreCaseSearchValues.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Wasm; using System.Runtime.Intrinsics.X86; namespace System.Buffers @@ -33,11 +34,13 @@ internal override bool ContainsCore(char value) => [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAny(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyIgnoreCase(ref MemoryMarshal.GetReference(span), _lowerCase, span.Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAnyExcept(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyExceptIgnoreCase(ref MemoryMarshal.GetReference(span), _lowerCase, span.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedSearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedSearchValues.cs index e6aa0c870486d8..92f516faf20505 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedSearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any1CharPackedSearchValues.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Wasm; using System.Runtime.Intrinsics.X86; namespace System.Buffers @@ -23,11 +24,13 @@ internal override bool ContainsCore(char value) => [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAny(ReadOnlySpan span) => PackedSpanHelpers.IndexOf(ref MemoryMarshal.GetReference(span), _e0, span.Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAnyExcept(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyExcept(ref MemoryMarshal.GetReference(span), _e0, span.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedIgnoreCaseSearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedIgnoreCaseSearchValues.cs index 7b789febead068..7a3602dd1cbd99 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedIgnoreCaseSearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedIgnoreCaseSearchValues.cs @@ -40,11 +40,13 @@ internal override bool ContainsCore(char value) [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAny(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyIgnoreCase(ref MemoryMarshal.GetReference(span), _e0, _e1, span.Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAnyExcept(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyExceptIgnoreCase(ref MemoryMarshal.GetReference(span), _e0, _e1, span.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedSearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedSearchValues.cs index d951b8e2375e0f..3088a1fc9604ba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedSearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any2CharPackedSearchValues.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Wasm; using System.Runtime.Intrinsics.X86; namespace System.Buffers @@ -23,11 +24,13 @@ internal override bool ContainsCore(char value) => [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAny(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAny(ref MemoryMarshal.GetReference(span), _e0, _e1, span.Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAnyExcept(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyExcept(ref MemoryMarshal.GetReference(span), _e0, _e1, span.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any3CharPackedSearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any3CharPackedSearchValues.cs index cebb695aa3fb79..9a5f24bfc880b2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any3CharPackedSearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Any3CharPackedSearchValues.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.Wasm; using System.Runtime.Intrinsics.X86; namespace System.Buffers @@ -23,11 +24,13 @@ internal override bool ContainsCore(char value) => [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAny(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAny(ref MemoryMarshal.GetReference(span), _e0, _e1, _e2, span.Length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal override int IndexOfAnyExcept(ReadOnlySpan span) => PackedSpanHelpers.IndexOfAnyExcept(ref MemoryMarshal.GetReference(span), _e0, _e1, _e2, span.Length); diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs index 5c1d64bf9b0dfb..7f5dcb2b8662e1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.Wasm; using System.Runtime.Intrinsics.X86; using static System.Buffers.StringSearchValuesHelper; @@ -36,20 +37,20 @@ internal sealed class SingleStringSearchValuesPackedThreeChars - Sse2.IsSupported && + (Sse2.IsSupported || PackedSimd.IsSupported) && typeof(TValueLength) == typeof(ValueLengthLessThan4) && (typeof(TCaseSensitivity) == typeof(CaseSensitive) || typeof(TCaseSensitivity) == typeof(CaseInsensitiveAsciiLetters)); } public SingleStringSearchValuesPackedThreeChars(HashSet? uniqueValues, string value, int ch2Offset, int ch3Offset) : base(uniqueValues) { - Debug.Assert(Sse2.IsSupported || AdvSimd.Arm64.IsSupported); + Debug.Assert(Sse2.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported); // We could have more than one entry in 'uniqueValues' if this value is an exact prefix of all the others. Debug.Assert(value.Length > 1); @@ -182,7 +183,7 @@ private int IndexOf(ref char searchSpace, int searchSpaceLength) goto LoopFooter; } } - else if ((Sse2.IsSupported || AdvSimd.Arm64.IsSupported) && searchSpaceMinusValueTailLength - Vector128.Count >= 0) + else if ((Sse2.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported) && searchSpaceMinusValueTailLength - Vector128.Count >= 0) { Vector128 ch1 = Vector128.Create(_ch1); Vector128 ch2 = Vector128.Create(_ch2); @@ -250,6 +251,7 @@ private int IndexOf(ref char searchSpace, int searchSpaceLength) [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + [CompExactlyDependsOn(typeof(PackedSimd))] private static Vector128 GetComparisonResult(ref char searchSpace, nuint ch2ByteOffset, nuint ch3ByteOffset, Vector128 ch1, Vector128 ch2, Vector128 ch3) { // Load 3 vectors from the input. @@ -390,6 +392,7 @@ internal override string[] GetValues() => HasUniqueValues [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] [CompExactlyDependsOn(typeof(AdvSimd.Arm64))] + [CompExactlyDependsOn(typeof(PackedSimd))] private static Vector128 LoadPacked128(ref char searchSpace, nuint byteOffset) { Vector128 input0 = Vector128.LoadUnsafe(ref Unsafe.AddByteOffset(ref searchSpace, byteOffset)); @@ -397,7 +400,9 @@ private static Vector128 LoadPacked128(ref char searchSpace, nuint byteOff return Sse2.IsSupported ? Sse2.PackUnsignedSaturate(input0.AsInt16(), input1.AsInt16()) - : AdvSimd.Arm64.UnzipEven(input0.AsByte(), input1.AsByte()); + : PackedSimd.IsSupported + ? PackedSimd.ConvertNarrowingSaturateUnsigned(input0.AsInt16(), input1.AsInt16()) + : AdvSimd.Arm64.UnzipEven(input0.AsByte(), input1.AsByte()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/StringSearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/StringSearchValues.cs index dcfb8e66275b83..e0a8b21d52085c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/StringSearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/StringSearchValues.cs @@ -444,8 +444,8 @@ private static SearchValues CreateSingleValuesThreeChars(uniqueValues, value, ch2Offset, ch3Offset); - // Unlike with PackedSpanHelpers (Sse2 only), we are also using this approach on ARM64. - // We use PackUnsignedSaturate on X86 and UnzipEven on ARM, so the set of allowed characters differs slightly (we can't use it for \0 and \xFF on X86). + // Unlike with PackedSpanHelpers (Sse2 and PackedSimd only), we are also using this approach on ARM64. + // We use saturating packing on X86 and WASM and UnzipEven on ARM, so the set of allowed characters differs slightly (we can't use it for \0 and \xFF on X86 and WASM). static bool CanUsePackedImpl(char c) => PackedSpanHelpers.PackedIndexOfIsSupported ? PackedSpanHelpers.CanUsePackedIndexOf(c) : (AdvSimd.Arm64.IsSupported && c <= byte.MaxValue); diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs index be05f8dca2bd00..120a6ac2c01521 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Wasm; using System.Runtime.Intrinsics.X86; namespace System @@ -13,9 +14,9 @@ namespace System // included in this file which are specific to the packed implementation. internal static partial class PackedSpanHelpers { - // We only do this optimization if we have support for X86 intrinsics (Sse2) as the packing is noticeably cheaper compared to ARM (AdvSimd). + // We only do this optimization if we have support for X86 intrinsics (Sse2) or WASM SIMD (PackedSimd) as the packing is noticeably cheaper compared to ARM (AdvSimd). // While the impact on the worst-case (match at the start) is minimal on X86, it's prohibitively large on ARM. - public static bool PackedIndexOfIsSupported => Sse2.IsSupported; + public static bool PackedIndexOfIsSupported => Sse2.IsSupported || PackedSimd.IsSupported; // Not all values can benefit from packing the searchSpace. See comments in PackSources below. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -30,36 +31,43 @@ public static unsafe bool CanUsePackedIndexOf(T value) [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOf(ref char searchSpace, char value, int length) => IndexOf, NopTransform>(ref Unsafe.As(ref searchSpace), (short)value, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyExcept(ref char searchSpace, char value, int length) => IndexOf, NopTransform>(ref Unsafe.As(ref searchSpace), (short)value, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAny(ref char searchSpace, char value0, char value1, int length) => IndexOfAny, NopTransform>(ref Unsafe.As(ref searchSpace), (short)value0, (short)value1, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyExcept(ref char searchSpace, char value0, char value1, int length) => IndexOfAny, NopTransform>(ref Unsafe.As(ref searchSpace), (short)value0, (short)value1, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAny(ref char searchSpace, char value0, char value1, char value2, int length) => IndexOfAny>(ref Unsafe.As(ref searchSpace), (short)value0, (short)value1, (short)value2, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyExcept(ref char searchSpace, char value0, char value1, char value2, int length) => IndexOfAny>(ref Unsafe.As(ref searchSpace), (short)value0, (short)value1, (short)value2, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyIgnoreCase(ref char searchSpace, char value, int length) { Debug.Assert((value | 0x20) == value); @@ -69,6 +77,7 @@ public static int IndexOfAnyIgnoreCase(ref char searchSpace, char value, int len [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyExceptIgnoreCase(ref char searchSpace, char value, int length) { Debug.Assert((value | 0x20) == value); @@ -78,6 +87,7 @@ public static int IndexOfAnyExceptIgnoreCase(ref char searchSpace, char value, i [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyIgnoreCase(ref char searchSpace, char value0, char value1, int length) { Debug.Assert((value0 | 0x20) == value0); @@ -88,6 +98,7 @@ public static int IndexOfAnyIgnoreCase(ref char searchSpace, char value0, char v [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyExceptIgnoreCase(ref char searchSpace, char value0, char value1, int length) { Debug.Assert((value0 | 0x20) == value0); @@ -98,15 +109,18 @@ public static int IndexOfAnyExceptIgnoreCase(ref char searchSpace, char value0, [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyInRange(ref char searchSpace, char lowInclusive, char rangeInclusive, int length) => IndexOfAnyInRange>(ref Unsafe.As(ref searchSpace), (short)lowInclusive, (short)rangeInclusive, length); [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static int IndexOfAnyExceptInRange(ref char searchSpace, char lowInclusive, char rangeInclusive, int length) => IndexOfAnyInRange>(ref Unsafe.As(ref searchSpace), (short)lowInclusive, (short)rangeInclusive, length); [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] public static bool Contains(ref short searchSpace, short value, int length) { Debug.Assert(CanUsePackedIndexOf(value)); @@ -309,6 +323,7 @@ public static bool Contains(ref short searchSpace, short value, int length) } [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] private static int IndexOf(ref short searchSpace, short value, int length) where TNegator : struct, SpanHelpers.INegator where TTransform : struct, ITransform @@ -512,6 +527,7 @@ private static int IndexOf(ref short searchSpace, short va } [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] private static int IndexOfAny(ref short searchSpace, short value0, short value1, int length) where TNegator : struct, SpanHelpers.INegator where TTransform : struct, ITransform @@ -726,6 +742,7 @@ private static int IndexOfAny(ref short searchSpace, short } [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] private static int IndexOfAny(ref short searchSpace, short value0, short value1, short value2, int length) where TNegator : struct, SpanHelpers.INegator { @@ -944,6 +961,7 @@ private static int IndexOfAny(ref short searchSpace, short value0, sho } [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] private static int IndexOfAnyInRange(ref short searchSpace, short lowInclusive, short rangeInclusive, int length) where TNegator : struct, SpanHelpers.INegator { @@ -1164,14 +1182,17 @@ internal static Vector256 PackSources(Vector256 source0, Vector256< [MethodImpl(MethodImplOptions.AggressiveInlining)] [CompExactlyDependsOn(typeof(Sse2))] + [CompExactlyDependsOn(typeof(PackedSimd))] internal static Vector128 PackSources(Vector128 source0, Vector128 source1) { - Debug.Assert(Sse2.IsSupported); + Debug.Assert(Sse2.IsSupported || PackedSimd.IsSupported); // Pack two vectors of characters into bytes. While the type is Vector128, these are really UInt16 characters. - // X86: Downcast every character using saturation. + // X86 and WASM: Downcast every character using saturation. // - Values <= 32767 result in min(value, 255). // - Values > 32767 result in 0. Because of this we can't accept needles that contain 0. - return Sse2.PackUnsignedSaturate(source0, source1).AsByte(); + return Sse2.IsSupported + ? Sse2.PackUnsignedSaturate(source0, source1).AsByte() + : PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index f3a2c16a910579..2638ad3a2da291 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -2322,7 +2322,7 @@ private static void MakeSeparatorListVectorized(ReadOnlySpan sourceSpan, r Vector128 v2 = Vector128.Create((ushort)c2); Vector128 v3 = Vector128.Create((ushort)c3); - if (Sse2.IsSupported && PackedSpanHelpers.CanUsePackedIndexOf(c) && PackedSpanHelpers.CanUsePackedIndexOf(c2) && PackedSpanHelpers.CanUsePackedIndexOf(c3)) + if (PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(c) && PackedSpanHelpers.CanUsePackedIndexOf(c2) && PackedSpanHelpers.CanUsePackedIndexOf(c3)) { // Process in double chunks & check if either chunk is likely to contain matches at once. // If we get multiple matches in a single chunk, we assume they're likely to be close & From d0fd7373c2b28c39aa3740eebe046da1eb007028 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 15 Oct 2026 15:55:23 -0500 Subject: [PATCH 2/4] Use explicit IsSupported chains for packed loads 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 --- ...ingleStringSearchValuesPackedThreeChars.cs | 23 +++++++++++++++---- .../src/System/SpanHelpers.Packed.cs | 18 +++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs index 7f5dcb2b8662e1..8425ce899fdc45 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/SingleStringSearchValuesPackedThreeChars.cs @@ -398,11 +398,24 @@ private static Vector128 LoadPacked128(ref char searchSpace, nuint byteOff Vector128 input0 = Vector128.LoadUnsafe(ref Unsafe.AddByteOffset(ref searchSpace, byteOffset)); Vector128 input1 = Vector128.LoadUnsafe(ref Unsafe.AddByteOffset(ref searchSpace, byteOffset + (uint)Vector128.Count)); - return Sse2.IsSupported - ? Sse2.PackUnsignedSaturate(input0.AsInt16(), input1.AsInt16()) - : PackedSimd.IsSupported - ? PackedSimd.ConvertNarrowingSaturateUnsigned(input0.AsInt16(), input1.AsInt16()) - : AdvSimd.Arm64.UnzipEven(input0.AsByte(), input1.AsByte()); + if (Sse2.IsSupported) + { + return Sse2.PackUnsignedSaturate(input0.AsInt16(), input1.AsInt16()); + } + else if (AdvSimd.Arm64.IsSupported) + { + return AdvSimd.Arm64.UnzipEven(input0.AsByte(), input1.AsByte()); + } + else if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateUnsigned(input0.AsInt16(), input1.AsInt16()); + } + else + { + // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are live/dead + ThrowHelper.ThrowUnreachableException(); + return default; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs index 120a6ac2c01521..bef79d5fca3a5a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs @@ -1185,14 +1185,24 @@ internal static Vector256 PackSources(Vector256 source0, Vector256< [CompExactlyDependsOn(typeof(PackedSimd))] internal static Vector128 PackSources(Vector128 source0, Vector128 source1) { - Debug.Assert(Sse2.IsSupported || PackedSimd.IsSupported); // Pack two vectors of characters into bytes. While the type is Vector128, these are really UInt16 characters. // X86 and WASM: Downcast every character using saturation. // - Values <= 32767 result in min(value, 255). // - Values > 32767 result in 0. Because of this we can't accept needles that contain 0. - return Sse2.IsSupported - ? Sse2.PackUnsignedSaturate(source0, source1).AsByte() - : PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); + if (Sse2.IsSupported) + { + return Sse2.PackUnsignedSaturate(source0, source1).AsByte(); + } + else if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); + } + else + { + // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are live/dead + ThrowHelper.ThrowUnreachableException(); + return default; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] From d92832c4cf8732e6a6d4a38847dbcfdd6a393ae5 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 16 Oct 2026 15:28:58 -0500 Subject: [PATCH 3/4] Simplify PackedSpanHelpers.PackSources back to a ternary 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 --- .../src/System/SpanHelpers.Packed.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs index bef79d5fca3a5a..120a6ac2c01521 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs @@ -1185,24 +1185,14 @@ internal static Vector256 PackSources(Vector256 source0, Vector256< [CompExactlyDependsOn(typeof(PackedSimd))] internal static Vector128 PackSources(Vector128 source0, Vector128 source1) { + Debug.Assert(Sse2.IsSupported || PackedSimd.IsSupported); // Pack two vectors of characters into bytes. While the type is Vector128, these are really UInt16 characters. // X86 and WASM: Downcast every character using saturation. // - Values <= 32767 result in min(value, 255). // - Values > 32767 result in 0. Because of this we can't accept needles that contain 0. - if (Sse2.IsSupported) - { - return Sse2.PackUnsignedSaturate(source0, source1).AsByte(); - } - else if (PackedSimd.IsSupported) - { - return PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); - } - else - { - // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are live/dead - ThrowHelper.ThrowUnreachableException(); - return default; - } + return Sse2.IsSupported + ? Sse2.PackUnsignedSaturate(source0, source1).AsByte() + : PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 056c9a2a2b593dba02559287a49c8af497267f5e Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 16 Oct 2026 16:28:59 -0500 Subject: [PATCH 4/4] Restore the explicit IsSupported chain in PackSources Reverts d92832c4cf8. 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 --- .../src/System/SpanHelpers.Packed.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs index 120a6ac2c01521..bef79d5fca3a5a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Packed.cs @@ -1185,14 +1185,24 @@ internal static Vector256 PackSources(Vector256 source0, Vector256< [CompExactlyDependsOn(typeof(PackedSimd))] internal static Vector128 PackSources(Vector128 source0, Vector128 source1) { - Debug.Assert(Sse2.IsSupported || PackedSimd.IsSupported); // Pack two vectors of characters into bytes. While the type is Vector128, these are really UInt16 characters. // X86 and WASM: Downcast every character using saturation. // - Values <= 32767 result in min(value, 255). // - Values > 32767 result in 0. Because of this we can't accept needles that contain 0. - return Sse2.IsSupported - ? Sse2.PackUnsignedSaturate(source0, source1).AsByte() - : PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); + if (Sse2.IsSupported) + { + return Sse2.PackUnsignedSaturate(source0, source1).AsByte(); + } + else if (PackedSimd.IsSupported) + { + return PackedSimd.ConvertNarrowingSaturateUnsigned(source0, source1); + } + else + { + // We explicitly recheck each IsSupported query to ensure that the trimmer can see which paths are live/dead + ThrowHelper.ThrowUnreachableException(); + return default; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)]