Skip to content

Commit 4933df6

Browse files
authored
Merge pull request #30 from rameel/improvements
Optimize PathSegmentIterator
2 parents 6a03687 + 49ed682 commit 4933df6

6 files changed

Lines changed: 68 additions & 56 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Ramstack.Globbing
2+
[![NuGet](https://img.shields.io/nuget/v/Ramstack.Globbing.svg)](https://nuget.org/packages/Ramstack.Globbing)
3+
[![MIT](https://img.shields.io/github/license/rameel/ramstack.globbing)](https://github.com/rameel/ramstack.globbing/blob/main/LICENSE)
24

35
Fast and zero-allocation .NET globbing library for matching file paths using [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)).
46
No external dependencies.
@@ -215,9 +217,9 @@ await foreach (string filePath in enumeration)
215217

216218
## Supported versions
217219

218-
| | Version |
219-
|------|---------|
220-
| .NET | 6, 7, 8 |
220+
| | Version |
221+
|------|------------|
222+
| .NET | 6, 7, 8, 9 |
221223

222224
## Contributions
223225

Ramstack.Globbing.Tests/SimdConfigurationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Runtime.Intrinsics.X86;
1+
using System.Runtime.Intrinsics.X86;
22

33
namespace Ramstack.Globbing;
44

Ramstack.Globbing.Tests/Traversal/PathHelperTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public partial class PathHelperTests
2121
[TestCase("directory_1/directory_2", 2)]
2222
[TestCase("directory_1/directory_2/", 2)]
2323
[TestCase("///directory_1/directory_2////", 2)]
24+
[TestCase("/1/2/3/4/5/6/project/src/tests", 9)]
2425
public void CountPathSegments(string path, int expected)
2526
{
2627
Assert.That(

Ramstack.Globbing/Internal/PathHelper.cs

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,18 @@ public static bool IsPartialMatch(ReadOnlySpan<char> path, string[] patterns, Ma
6666
public static int CountPathSegments(scoped ReadOnlySpan<char> path, MatchFlags flags)
6767
{
6868
var count = 0;
69+
var iterator = new PathSegmentIterator();
6970
ref var s = ref Unsafe.AsRef(in MemoryMarshal.GetReference(path));
70-
var iterator = new PathSegmentIterator(path.Length);
71+
var length = path.Length;
7172

7273
while (true)
7374
{
74-
var r = iterator.GetNext(ref s, flags);
75+
var r = iterator.GetNext(ref s, length, flags);
76+
7577
if (r.start != r.final)
7678
count++;
7779

78-
if (r.final == path.Length)
80+
if (r.final == length)
7981
break;
8082
}
8183

@@ -101,17 +103,18 @@ public static ReadOnlySpan<char> GetPartialPattern(string pattern, MatchFlags fl
101103
if (depth < 1)
102104
depth = 1;
103105

106+
var iterator = new PathSegmentIterator();
104107
ref var s = ref Unsafe.AsRef(in pattern.GetPinnableReference());
105-
var iterator = new PathSegmentIterator(pattern.Length);
108+
var length = pattern.Length;
106109

107110
while (true)
108111
{
109-
var r = iterator.GetNext(ref s, flags);
112+
var r = iterator.GetNext(ref s, length, flags);
110113
if (r.start != r.final)
111114
depth--;
112115

113116
if (depth < 1
114-
|| r.final == pattern.Length
117+
|| r.final == length
115118
|| IsGlobStar(ref s, r.start, r.final))
116119
return MemoryMarshal.CreateReadOnlySpan(ref s, r.final);
117120
}
@@ -197,24 +200,11 @@ static void ConvertPathToPosixStyleImpl(ref char p, nint length)
197200
/// </returns>
198201
private static Vector256<ushort> CreateAllowEscaping256Bitmask(MatchFlags flags)
199202
{
200-
// Here is a small trick to avoid branching.
201-
// To reduce the number of required instructions, we convert the value `Windows`,
202-
// which equals 2, into a bitmask that allows escaping characters.
203-
// Windows (2) (No character escaping):
204-
// 0000 0010 >> 1 = 0000 0001
205-
// 0000 0001 & 0000 0001 = 0000 0001
206-
// 0000 0001 - 1 = 0000 0000
207-
// Any other value will simply convert to 0.
208-
// Unix (4) (Allow escaping characters)
209-
// 0000 0100 >> 1 = 0000 0010
210-
// 0000 0010 & 0000 0001 = 0000 0000
211-
// 0000 0000 - 1 = 1111 1111
212-
// Next, during the check, we can simply use the Avx2.AndNot instruction instead of Avx2.And:
213-
// Avx2.AndNot(
214-
// allowEscaping,
215-
// Avx2.CompareEqual(chunk, backslash)))
216-
Debug.Assert(MatchFlags.Windows == (MatchFlags)2);
217-
return Vector256.Create(((uint)flags >> 1 & 1) - 1).AsUInt16();
203+
var mask = Vector256<ushort>.Zero;
204+
if (flags != MatchFlags.Windows)
205+
mask = Vector256<ushort>.AllBitsSet;
206+
207+
return mask;
218208
}
219209

220210
/// <summary>
@@ -226,8 +216,11 @@ private static Vector256<ushort> CreateAllowEscaping256Bitmask(MatchFlags flags)
226216
/// </returns>
227217
private static Vector128<ushort> CreateAllowEscaping128Bitmask(MatchFlags flags)
228218
{
229-
Debug.Assert(MatchFlags.Windows == (MatchFlags)2);
230-
return Vector128.Create(((uint)flags >> 1 & 1) - 1).AsUInt16();
219+
var mask = Vector128<ushort>.Zero;
220+
if (flags != MatchFlags.Windows)
221+
mask = Vector128<ushort>.AllBitsSet;
222+
223+
return mask;
231224
}
232225

233226
/// <summary>
@@ -278,23 +271,22 @@ ref Unsafe.As<char, byte>(ref Unsafe.Add(ref destination, offset)),
278271
/// </summary>
279272
private struct PathSegmentIterator
280273
{
281-
private nint _last;
274+
private int _last;
282275
private nint _position;
283276
private uint _mask;
284-
private readonly nint _length;
285277

286278
/// <summary>
287279
/// Initializes a new instance of the <see cref="PathSegmentIterator"/> structure.
288280
/// </summary>
289-
/// <param name="length">The path length.</param>
290281
[MethodImpl(MethodImplOptions.AggressiveInlining)]
291-
public PathSegmentIterator(int length) =>
292-
(_last, _length) = (-1, (nint)(uint)length);
282+
public PathSegmentIterator() =>
283+
_last = -1;
293284

294285
/// <summary>
295286
/// Retrieves the next segment of the path.
296287
/// </summary>
297288
/// <param name="source">A reference to the starting character of the path.</param>
289+
/// <param name="length">The total number of characters in the input path starting from <paramref name="source"/>.</param>
298290
/// <param name="flags">The flags indicating the type of path separators to match.</param>
299291
/// <returns>
300292
/// A tuple containing the start and end indices of the next path segment.
@@ -303,39 +295,49 @@ public PathSegmentIterator(int length) =>
303295
/// The end of the iteration is indicated by <c>final</c> being equal to the length of the path.
304296
/// </returns>
305297
[MethodImpl(MethodImplOptions.AggressiveInlining)]
306-
public (int start, int final) GetNext(ref char source, MatchFlags flags)
298+
public (int start, int final) GetNext(ref char source, int length, MatchFlags flags)
307299
{
308-
//
309-
// Number of bits per char (ushort) in the MoveMask output
310-
//
311-
const uint BitsPerChar = 0b11;
312-
313300
var start = _last + 1;
314301

315-
while (_position < _length)
302+
while ((int)_position < length)
316303
{
317304
if ((Avx2.IsSupported || Sse2.IsSupported) && _mask != 0)
318305
{
319306
var offset = BitOperations.TrailingZeroCount(_mask);
320-
_last = _position + (nint)((uint)offset >> 1);
307+
_last = (int)(_position + (nint)((uint)offset >> 1));
321308

322309
//
323310
// Clear the bits for the current separator to process the next position in the mask
324311
//
325-
_mask &= ~(BitsPerChar << offset);
312+
_mask &= ~(0b_11u << offset);
326313

327314
//
328315
// Advance position to the next chunk when no separators remain in the mask
329316
//
330317
if (_mask == 0)
331-
_position += Avx2.IsSupported
318+
{
319+
//
320+
// https://github.com/dotnet/runtime/issues/117416
321+
//
322+
// Precompute the stride size instead of calculating it inline
323+
// to avoid stack spilling. For some unknown reason, the JIT
324+
// fails to optimize properly when this is written inline, like so:
325+
// _position += Avx2.IsSupported
326+
// ? Vector256<ushort>.Count
327+
// : Vector128<ushort>.Count;
328+
//
329+
330+
var stride = Avx2.IsSupported
332331
? Vector256<ushort>.Count
333332
: Vector128<ushort>.Count;
334333

335-
return ((int)start, (int)_last);
334+
_position += stride;
335+
}
336+
337+
return (start, _last);
336338
}
337339

338-
if (Avx2.IsSupported && _position + Vector256<ushort>.Count <= _length)
340+
if (Avx2.IsSupported && (int)_position + Vector256<ushort>.Count <= length)
339341
{
340342
var chunk = LoadVector256(ref source, _position);
341343
var allowEscapingMask = CreateAllowEscaping256Bitmask(flags);
@@ -362,7 +364,7 @@ public PathSegmentIterator(int length) =>
362364
if (_mask == 0)
363365
_position += Vector256<ushort>.Count;
364366
}
365-
else if (Sse2.IsSupported && !Avx2.IsSupported && _position + Vector128<ushort>.Count <= _length)
367+
else if (Sse2.IsSupported && !Avx2.IsSupported && (int)_position + Vector128<ushort>.Count <= length)
366368
{
367369
var chunk = LoadVector128(ref source, _position);
368370
var allowEscapingMask = CreateAllowEscaping128Bitmask(flags);
@@ -391,20 +393,21 @@ public PathSegmentIterator(int length) =>
391393
}
392394
else
393395
{
394-
for (; _position < _length; _position++)
396+
for (; (int)_position < length; _position++)
395397
{
396398
var ch = Unsafe.Add(ref source, _position);
397399
if (ch == '/' || (ch == '\\' && flags == MatchFlags.Windows))
398400
{
399-
_last = _position;
401+
_last = (int)_position;
400402
_position++;
401-
return ((int)start, (int)_last);
403+
404+
return (start, _last);
402405
}
403406
}
404407
}
405408
}
406409

407-
return ((int)start, (int)_length);
410+
return (start, length);
408411
}
409412
}
410413

Ramstack.Globbing/Traversal/FileTreeAsyncEnumerable.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ IAsyncEnumerator<TResult> IAsyncEnumerable<TResult>.GetAsyncEnumerator(Cancellat
8484

8585
private async IAsyncEnumerable<TResult> EnumerateAsync(CancellationTokenSource? source, [EnumeratorCancellation] CancellationToken cancellationToken)
8686
{
87-
var chars = ArrayPool<char>.Shared.Rent(512);
88-
8987
try
9088
{
89+
var chars = ArrayPool<char>.Shared.Rent(FileTreeEnumerable<TEntry, TResult>.DefaultBufferCapacity);
90+
9191
var queue = new Queue<(TEntry Directory, string Path)>();
9292
queue.Enqueue((_directory, ""));
9393

@@ -110,10 +110,11 @@ private async IAsyncEnumerable<TResult> EnumerateAsync(CancellationTokenSource?
110110
yield return ResultSelector(entry);
111111
}
112112
}
113+
114+
ArrayPool<char>.Shared.Return(chars);
113115
}
114116
finally
115117
{
116-
ArrayPool<char>.Shared.Return(chars);
117118
source?.Dispose();
118119
}
119120
}

Ramstack.Globbing/Traversal/FileTreeEnumerable.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public sealed class FileTreeEnumerable<TEntry, TResult> : IEnumerable<TResult>
1414
{
1515
private readonly TEntry _directory;
1616

17+
/// <summary>
18+
/// The default capacity of the character buffer for paths rented from the shared array pool.
19+
/// </summary>
20+
internal const int DefaultBufferCapacity = 512;
21+
1722
/// <summary>
1823
/// Gets or sets the glob patterns to include in the enumeration.
1924
/// </summary>
@@ -72,7 +77,7 @@ IEnumerator IEnumerable.GetEnumerator() =>
7277

7378
private IEnumerable<TResult> Enumerate()
7479
{
75-
var chars = ArrayPool<char>.Shared.Rent(512);
80+
var chars = ArrayPool<char>.Shared.Rent(DefaultBufferCapacity);
7681

7782
var queue = new Queue<(TEntry Directory, string Path)>();
7883
queue.Enqueue((_directory, ""));

0 commit comments

Comments
 (0)