Skip to content

Commit a1d1a8d

Browse files
Add wasm support to IndexOfAnyAsciiSearcher (#83122)
* Add wasm support to IndexOfAnyAsciiSearcher * Update with newly-added PackedSimd.ConvertNarrowing methods --------- Co-authored-by: Miha Zupan <[email protected]>
1 parent 9d6e592 commit a1d1a8d

File tree

2 files changed

+35
-27
lines changed

2 files changed

+35
-27
lines changed

src/libraries/System.Private.CoreLib/src/System/IndexOfAnyValues/IndexOfAnyAsciiSearcher.cs

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Buffers.Binary;
54
using System.Diagnostics;
65
using System.Numerics;
76
using System.Runtime.CompilerServices;
87
using System.Runtime.Intrinsics;
98
using System.Runtime.Intrinsics.Arm;
9+
using System.Runtime.Intrinsics.Wasm;
1010
using System.Runtime.Intrinsics.X86;
1111

1212
#pragma warning disable 8500 // sizeof of managed types
13+
#pragma warning disable IDE0060 // https://github.com/dotnet/roslyn-analyzers/issues/6228
1314

1415
namespace System.Buffers
1516
{
1617
internal static class IndexOfAnyAsciiSearcher
1718
{
18-
internal static bool IsVectorizationSupported => Ssse3.IsSupported || AdvSimd.Arm64.IsSupported;
19+
internal static bool IsVectorizationSupported => Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported;
1920

2021
internal static unsafe void ComputeBitmap256(ReadOnlySpan<byte> values, out Vector128<byte> bitmap0, out Vector128<byte> bitmap1, out BitVector256 lookup)
2122
{
@@ -131,8 +132,8 @@ private static unsafe bool TryIndexOfAny<TNegator>(ref short searchSpace, int se
131132
Vector128<byte> bitmap = default;
132133
if (TryComputeBitmap(asciiValues, (byte*)&bitmap, out bool needleContainsZero))
133134
{
134-
index = Ssse3.IsSupported && needleContainsZero
135-
? IndexOfAnyVectorized<TNegator, Ssse3HandleZeroInNeedle>(ref searchSpace, searchSpaceLength, bitmap)
135+
index = (Ssse3.IsSupported || PackedSimd.IsSupported) && needleContainsZero
136+
? IndexOfAnyVectorized<TNegator, Ssse3AndWasmHandleZeroInNeedle>(ref searchSpace, searchSpaceLength, bitmap)
136137
: IndexOfAnyVectorized<TNegator, Default>(ref searchSpace, searchSpaceLength, bitmap);
137138
return true;
138139
}
@@ -153,8 +154,8 @@ private static unsafe bool TryLastIndexOfAny<TNegator>(ref short searchSpace, in
153154
Vector128<byte> bitmap = default;
154155
if (TryComputeBitmap(asciiValues, (byte*)&bitmap, out bool needleContainsZero))
155156
{
156-
index = Ssse3.IsSupported && needleContainsZero
157-
? LastIndexOfAnyVectorized<TNegator, Ssse3HandleZeroInNeedle>(ref searchSpace, searchSpaceLength, bitmap)
157+
index = (Ssse3.IsSupported || PackedSimd.IsSupported) && needleContainsZero
158+
? LastIndexOfAnyVectorized<TNegator, Ssse3AndWasmHandleZeroInNeedle>(ref searchSpace, searchSpaceLength, bitmap)
158159
: LastIndexOfAnyVectorized<TNegator, Default>(ref searchSpace, searchSpaceLength, bitmap);
159160
return true;
160161
}
@@ -812,26 +813,29 @@ private static Vector128<byte> IndexOfAnyLookup<TNegator, TOptimizations>(Vector
812813
where TOptimizations : struct, IOptimizations
813814
{
814815
// Pack two vectors of characters into bytes. While the type is Vector128<short>, these are really UInt16 characters.
815-
// X86: Downcast every character using saturation.
816+
// X86 and WASM: Downcast every character using saturation.
816817
// - Values <= 32767 result in min(value, 255).
817818
// - Values > 32767 result in 0. Because of this we must do more work to handle needles that contain 0.
818819
// ARM64: Do narrowing saturation over unsigned values.
819820
// - All values result in min(value, 255)
820-
Vector128<byte> source = Sse2.IsSupported
821-
? Sse2.PackUnsignedSaturate(source0, source1)
822-
: AdvSimd.ExtractNarrowingSaturateUpper(AdvSimd.ExtractNarrowingSaturateLower(source0.AsUInt16()), source1.AsUInt16());
821+
Vector128<byte> source =
822+
Sse2.IsSupported ? Sse2.PackUnsignedSaturate(source0, source1) :
823+
AdvSimd.IsSupported ? AdvSimd.ExtractNarrowingSaturateUpper(AdvSimd.ExtractNarrowingSaturateLower(source0.AsUInt16()), source1.AsUInt16()) :
824+
PackedSimd.ConvertNarrowingUnsignedSaturate(source0, source1);
823825

824826
Vector128<byte> result = IndexOfAnyLookupCore(source, bitmapLookup);
825827

826-
// On X86, PackUnsignedSaturate resulted in values becoming 0 for inputs above 32767.
828+
// On X86 and WASM, the packing/narrowing above resulted in values becoming 0 for inputs above 32767.
827829
// Any value above 32767 would therefore match against 0. If 0 is present in the needle, we must clear the false positives.
828-
// In both cases, we can correct the result by clearing any bits that matched with a non-ascii source character.
830+
// We can correct the result by clearing any bits that matched with a non-ascii source character.
829831
if (TOptimizations.NeedleContainsZero)
830832
{
831-
Debug.Assert(Sse2.IsSupported);
833+
Debug.Assert(Sse2.IsSupported || PackedSimd.IsSupported);
832834
Vector128<short> ascii0 = Vector128.LessThan(source0.AsUInt16(), Vector128.Create((ushort)128)).AsInt16();
833835
Vector128<short> ascii1 = Vector128.LessThan(source1.AsUInt16(), Vector128.Create((ushort)128)).AsInt16();
834-
Vector128<byte> ascii = Sse2.PackSignedSaturate(ascii0, ascii1).AsByte();
836+
Vector128<byte> ascii = Sse2.IsSupported
837+
? Sse2.PackSignedSaturate(ascii0, ascii1).AsByte()
838+
: PackedSimd.ConvertNarrowingSignedSaturate(ascii0, ascii1).AsByte();
835839
result &= ascii;
836840
}
837841

@@ -850,15 +854,21 @@ private static Vector128<byte> IndexOfAnyLookupCore(Vector128<byte> source, Vect
850854
// On ARM, we have an instruction for an arithmetic right shift of 1-byte signed values.
851855
// The shift will map values above 127 to values above 16, which the shuffle will then map to 0.
852856
// This is how we exclude non-ASCII values from results on ARM.
853-
// On X86, use a 4-byte value shift with AND 15 to emulate a 1-byte value logical shift.
857+
// On X86 and WASM, use a 4-byte value shift with AND 15 to emulate a 1-byte value logical shift.
854858
Vector128<byte> highNibbles = AdvSimd.IsSupported
855859
? AdvSimd.ShiftRightArithmetic(source.AsSByte(), 4).AsByte()
856-
: Sse2.ShiftRightLogical(source.AsInt32(), 4).AsByte() & Vector128.Create((byte)0xF);
860+
: (source.AsInt32() >>> 4).AsByte() & Vector128.Create((byte)0xF);
857861

858862
// The bitmapLookup represents a 8x16 table of bits, indicating whether a character is present in the needle.
859863
// Lookup the rows via the lower nibble and the column via the higher nibble.
860864
Vector128<byte> bitMask = Shuffle(bitmapLookup, lowNibbles);
861-
Vector128<byte> bitPositions = Shuffle(Vector128.Create(0x8040201008040201).AsByte(), highNibbles);
865+
866+
// On WASM, we still have to handle values outside the ASCII range.
867+
// For values above 127, the high nibble will be above 7. We construct the positions vector for the shuffle such that those values map to 0.
868+
// This is similar to how we use Ssse3's Shuffle characteristics on X86 to transform values above 127 to 0 by not masking the low nibbles above.
869+
Vector128<byte> bitPositions = PackedSimd.IsSupported
870+
? Shuffle(Vector128.Create(0x8040201008040201, 0).AsByte(), highNibbles)
871+
: Shuffle(Vector128.Create(0x8040201008040201).AsByte(), highNibbles);
862872

863873
Vector128<byte> result = bitMask & bitPositions;
864874
return result;
@@ -943,9 +953,10 @@ private static Vector256<byte> IndexOfAnyLookup<TNegator>(Vector256<byte> source
943953
private static Vector128<byte> Shuffle(Vector128<byte> vector, Vector128<byte> indices)
944954
{
945955
// We're not using Vector128.Shuffle as the caller already accounts for and relies on differences in behavior between platforms.
946-
return Ssse3.IsSupported
947-
? Ssse3.Shuffle(vector, indices)
948-
: AdvSimd.Arm64.VectorTableLookup(vector, indices);
956+
return
957+
Ssse3.IsSupported ? Ssse3.Shuffle(vector, indices) :
958+
AdvSimd.Arm64.IsSupported ? AdvSimd.Arm64.VectorTableLookup(vector, indices) :
959+
PackedSimd.Swizzle(vector, indices);
949960
}
950961

951962
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -957,7 +968,6 @@ private static unsafe int ComputeFirstIndex<T, TNegator>(ref T searchSpace, ref
957968
return offsetInVector + (int)(Unsafe.ByteOffset(ref searchSpace, ref current) / sizeof(T));
958969
}
959970

960-
#pragma warning disable IDE0060 // https://github.com/dotnet/roslyn-analyzers/issues/6228
961971
[MethodImpl(MethodImplOptions.AggressiveInlining)]
962972
private static unsafe int ComputeFirstIndexOverlapped<T, TNegator>(ref T searchSpace, ref T current0, ref T current1, Vector128<byte> result)
963973
where TNegator : struct, INegator
@@ -972,7 +982,6 @@ private static unsafe int ComputeFirstIndexOverlapped<T, TNegator>(ref T searchS
972982
}
973983
return offsetInVector + (int)(Unsafe.ByteOffset(ref searchSpace, ref current0) / sizeof(T));
974984
}
975-
#pragma warning restore IDE0060 // https://github.com/dotnet/roslyn-analyzers/issues/6228
976985

977986
[MethodImpl(MethodImplOptions.AggressiveInlining)]
978987
private static unsafe int ComputeLastIndex<T, TNegator>(ref T searchSpace, ref T current, Vector128<byte> result)
@@ -1013,7 +1022,6 @@ private static unsafe int ComputeFirstIndex<T, TNegator>(ref T searchSpace, ref
10131022
return offsetInVector + (int)(Unsafe.ByteOffset(ref searchSpace, ref current) / sizeof(T));
10141023
}
10151024

1016-
#pragma warning disable IDE0060 // https://github.com/dotnet/roslyn-analyzers/issues/6228
10171025
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10181026
private static unsafe int ComputeFirstIndexOverlapped<T, TNegator>(ref T searchSpace, ref T current0, ref T current1, Vector256<byte> result)
10191027
where TNegator : struct, INegator
@@ -1034,7 +1042,6 @@ private static unsafe int ComputeFirstIndexOverlapped<T, TNegator>(ref T searchS
10341042
}
10351043
return offsetInVector + (int)(Unsafe.ByteOffset(ref searchSpace, ref current0) / sizeof(T));
10361044
}
1037-
#pragma warning restore IDE0060 // https://github.com/dotnet/roslyn-analyzers/issues/6228
10381045

10391046
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10401047
private static unsafe int ComputeLastIndex<T, TNegator>(ref T searchSpace, ref T current, Vector256<byte> result)
@@ -1117,7 +1124,7 @@ internal interface IOptimizations
11171124
static abstract bool NeedleContainsZero { get; }
11181125
}
11191126

1120-
internal readonly struct Ssse3HandleZeroInNeedle : IOptimizations
1127+
internal readonly struct Ssse3AndWasmHandleZeroInNeedle : IOptimizations
11211128
{
11221129
public static bool NeedleContainsZero => true;
11231130
}

src/libraries/System.Private.CoreLib/src/System/IndexOfAnyValues/IndexOfAnyValues.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Runtime.CompilerServices;
77
using System.Runtime.InteropServices;
88
using System.Runtime.Intrinsics;
9+
using System.Runtime.Intrinsics.Wasm;
910
using System.Runtime.Intrinsics.X86;
1011

1112
#pragma warning disable 8500 // address of managed types
@@ -113,8 +114,8 @@ public static IndexOfAnyValues<char> Create(ReadOnlySpan<char> values)
113114
{
114115
IndexOfAnyAsciiSearcher.ComputeBitmap(values, out Vector128<byte> bitmap, out BitVector256 lookup);
115116

116-
return Ssse3.IsSupported && lookup.Contains(0)
117-
? new IndexOfAnyAsciiCharValues<IndexOfAnyAsciiSearcher.Ssse3HandleZeroInNeedle>(bitmap, lookup)
117+
return (Ssse3.IsSupported || PackedSimd.IsSupported) && lookup.Contains(0)
118+
? new IndexOfAnyAsciiCharValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle>(bitmap, lookup)
118119
: new IndexOfAnyAsciiCharValues<IndexOfAnyAsciiSearcher.Default>(bitmap, lookup);
119120
}
120121

0 commit comments

Comments
 (0)