Skip to content

Commit 232c6cf

Browse files
committed
Vectorized HttpUserAgentParser.TryExtractVersion
1 parent 4a82130 commit 232c6cf

File tree

2 files changed

+188
-28
lines changed

2 files changed

+188
-28
lines changed

src/HttpUserAgentParser/HttpUserAgentParser.cs

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright © https://myCSharp.de - all rights reserved
22

3+
using System.Diagnostics;
34
using System.Diagnostics.CodeAnalysis;
45
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
7+
using System.Runtime.Intrinsics;
58

69
namespace MyCSharp.HttpUserAgentParser;
710

@@ -206,45 +209,124 @@ private static bool TryExtractVersion(ReadOnlySpan<char> haystack, out Range ran
206209
{
207210
range = default;
208211

209-
// Limit search window to avoid scanning entire UA string unnecessarily
210-
const int Window = 128;
211-
if (haystack.Length > Window)
212+
if (Vector256.IsHardwareAccelerated && haystack.Length >= 2 * Vector256<short>.Count)
212213
{
213-
haystack = haystack.Slice(0, Window);
214-
}
214+
ref char ptr = ref MemoryMarshal.GetReference(haystack);
215215

216-
// Find first digit
217-
int start = -1;
218-
for (int i = 0; i < haystack.Length; i++)
219-
{
220-
char c = haystack[i];
221-
if (c >= '0' && c <= '9')
216+
Vector256<byte> vec = ptr.ReadVector256AsBytes(0);
217+
Vector256<byte> between0and9 = Vector256.LessThan(vec - Vector256.Create((byte)'0'), Vector256.Create((byte)('9' - '0' + 1)));
218+
219+
if (between0and9 == Vector256<byte>.Zero)
222220
{
223-
start = i;
224-
break;
221+
goto Scalar;
225222
}
226-
}
227223

228-
if (start < 0)
224+
uint bitMask = between0and9.ExtractMostSignificantBits();
225+
int idx = (int)uint.TrailingZeroCount(bitMask);
226+
Debug.Assert(idx is >= 0 and <= 32);
227+
int start = idx;
228+
229+
Vector256<byte> byteMask = between0and9 | Vector256.Equals(vec, Vector256.Create((byte)'.'));
230+
byteMask = ~byteMask;
231+
232+
if (byteMask == Vector256<byte>.Zero)
233+
{
234+
goto Scalar;
235+
}
236+
237+
bitMask = byteMask.ExtractMostSignificantBits();
238+
bitMask >>= start;
239+
240+
idx = start + (int)uint.TrailingZeroCount(bitMask);
241+
Debug.Assert(idx is >= 0 and <= 32);
242+
int end = idx;
243+
244+
range = new Range(start, end);
245+
return true;
246+
}
247+
else if (Vector128.IsHardwareAccelerated && haystack.Length >= 2 * Vector128<short>.Count)
229248
{
230-
// No digit found => no version
231-
return false;
249+
ref char ptr = ref MemoryMarshal.GetReference(haystack);
250+
251+
Vector128<byte> vec = ptr.ReadVector128AsBytes(0);
252+
Vector128<byte> between0and9 = Vector128.LessThan(vec - Vector128.Create((byte)'0'), Vector128.Create((byte)('9' - '0' + 1)));
253+
254+
if (between0and9 == Vector128<byte>.Zero)
255+
{
256+
goto Scalar;
257+
}
258+
259+
uint bitMask = between0and9.ExtractMostSignificantBits();
260+
int idx = (int)uint.TrailingZeroCount(bitMask);
261+
Debug.Assert(idx is >= 0 and <= 16);
262+
int start = idx;
263+
264+
Vector128<byte> byteMask = between0and9 | Vector128.Equals(vec, Vector128.Create((byte)'.'));
265+
byteMask = ~byteMask;
266+
267+
if (byteMask == Vector128<byte>.Zero)
268+
{
269+
goto Scalar;
270+
}
271+
272+
bitMask = byteMask.ExtractMostSignificantBits();
273+
bitMask >>= start;
274+
275+
idx = start + (int)uint.TrailingZeroCount(bitMask);
276+
Debug.Assert(idx is >= 0 and <= 16);
277+
int end = idx;
278+
279+
range = new Range(start, end);
280+
return true;
232281
}
233282

234-
// Consume digits and dots after first digit
235-
int end = start + 1;
236-
while (end < haystack.Length)
283+
Scalar:
237284
{
238-
char c = haystack[end];
239-
if (!((c >= '0' && c <= '9') || c == '.'))
285+
// Limit search window to avoid scanning entire UA string unnecessarily
286+
const int Windows = 128;
287+
if (haystack.Length > Windows)
240288
{
241-
break;
289+
haystack = haystack.Slice(0, Windows);
290+
}
291+
292+
int start = -1;
293+
int i = 0;
294+
295+
for (; i < haystack.Length; ++i)
296+
{
297+
char c = haystack[i];
298+
if (char.IsBetween(c, '0', '9'))
299+
{
300+
start = i;
301+
break;
302+
}
303+
}
304+
305+
if (start < 0)
306+
{
307+
// No digit found => no version
308+
return false;
309+
}
310+
311+
haystack = haystack.Slice(i + 1);
312+
for (i = 0; i < haystack.Length; ++i)
313+
{
314+
char c = haystack[i];
315+
if (!(char.IsBetween(c, '0', '9') || c == '.'))
316+
{
317+
break;
318+
}
242319
}
243-
end++;
244-
}
245320

246-
// Create exclusive end range
247-
range = new Range(start, end);
248-
return true;
321+
i += start + 1; // shift back the previous domain
322+
323+
if (i == start)
324+
{
325+
return false;
326+
}
327+
328+
range = new Range(start, i);
329+
return true;
330+
}
249331
}
250332
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright © https://myCSharp.de - all rights reserved
2+
3+
using System.Runtime.CompilerServices;
4+
using System.Runtime.Intrinsics;
5+
using System.Runtime.Intrinsics.Arm;
6+
using System.Runtime.Intrinsics.X86;
7+
8+
namespace MyCSharp.HttpUserAgentParser;
9+
10+
internal static class VectorExtensions
11+
{
12+
extension(ref char c)
13+
{
14+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
15+
public Vector128<byte> ReadVector128AsBytes(int offset)
16+
{
17+
ref short ptr = ref Unsafe.As<char, short>(ref c);
18+
19+
#if NET10_0_OR_GREATER
20+
return Vector128.NarrowWithSaturation(
21+
Vector128.LoadUnsafe(ref ptr, (uint)offset),
22+
Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128<short>.Count))
23+
).AsByte();
24+
#else
25+
if (Sse2.IsSupported)
26+
{
27+
return Sse2.PackUnsignedSaturate(
28+
Vector128.LoadUnsafe(ref ptr, (uint)offset),
29+
Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128<short>.Count)));
30+
}
31+
else if (AdvSimd.Arm64.IsSupported)
32+
{
33+
return AdvSimd.Arm64.UnzipEven(
34+
Vector128.LoadUnsafe(ref ptr, (uint)offset).AsByte(),
35+
Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128<short>.Count)).AsByte());
36+
}
37+
else
38+
{
39+
return Vector128.Narrow(
40+
Vector128.LoadUnsafe(ref ptr, (uint)offset),
41+
Vector128.LoadUnsafe(ref ptr, (uint)(offset + Vector128<short>.Count))
42+
).AsByte();
43+
}
44+
#endif
45+
}
46+
47+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
48+
public Vector256<byte> ReadVector256AsBytes(int offset)
49+
{
50+
ref short ptr = ref Unsafe.As<char, short>(ref c);
51+
52+
#if NET10_0_OR_GREATER
53+
return Vector256.NarrowWithSaturation(
54+
Vector256.LoadUnsafe(ref ptr, (uint)offset),
55+
Vector256.LoadUnsafe(ref ptr, (uint)offset + (uint)Vector256<short>.Count)
56+
).AsByte();
57+
#else
58+
if (Avx2.IsSupported)
59+
{
60+
Vector256<byte> tmp = Avx2.PackUnsignedSaturate(
61+
Vector256.LoadUnsafe(ref ptr, (uint)offset),
62+
Vector256.LoadUnsafe(ref ptr, (uint)offset + (uint)Vector256<short>.Count));
63+
64+
Vector256<long> tmp1 = Avx2.Permute4x64(tmp.AsInt64(), 0b_11_01_10_00);
65+
66+
return tmp1.AsByte();
67+
}
68+
else
69+
{
70+
return Vector256.Narrow(
71+
Vector256.LoadUnsafe(ref ptr, (uint)offset),
72+
Vector256.LoadUnsafe(ref ptr, (uint)offset + (uint)Vector256<short>.Count)
73+
).AsByte();
74+
}
75+
#endif
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)