diff --git a/README.md b/README.md index 21fa3f5..196e44a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Ramstack.Globbing +[![NuGet](https://img.shields.io/nuget/v/Ramstack.Globbing.svg)](https://nuget.org/packages/Ramstack.Globbing) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.globbing)](https://github.com/rameel/ramstack.globbing/blob/main/LICENSE) Fast and zero-allocation .NET globbing library for matching file paths using [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)). No external dependencies. @@ -215,9 +217,9 @@ await foreach (string filePath in enumeration) ## Supported versions -| | Version | -|------|---------| -| .NET | 6, 7, 8 | +| | Version | +|------|------------| +| .NET | 6, 7, 8, 9 | ## Contributions diff --git a/Ramstack.Globbing.Tests/SimdConfigurationTests.cs b/Ramstack.Globbing.Tests/SimdConfigurationTests.cs index 6ddb86a..ba541a2 100644 --- a/Ramstack.Globbing.Tests/SimdConfigurationTests.cs +++ b/Ramstack.Globbing.Tests/SimdConfigurationTests.cs @@ -1,4 +1,4 @@ -using System.Runtime.Intrinsics.X86; +using System.Runtime.Intrinsics.X86; namespace Ramstack.Globbing; diff --git a/Ramstack.Globbing.Tests/Traversal/PathHelperTests.cs b/Ramstack.Globbing.Tests/Traversal/PathHelperTests.cs index 6a65113..d452388 100644 --- a/Ramstack.Globbing.Tests/Traversal/PathHelperTests.cs +++ b/Ramstack.Globbing.Tests/Traversal/PathHelperTests.cs @@ -21,6 +21,7 @@ public partial class PathHelperTests [TestCase("directory_1/directory_2", 2)] [TestCase("directory_1/directory_2/", 2)] [TestCase("///directory_1/directory_2////", 2)] + [TestCase("/1/2/3/4/5/6/project/src/tests", 9)] public void CountPathSegments(string path, int expected) { Assert.That( diff --git a/Ramstack.Globbing/Internal/PathHelper.cs b/Ramstack.Globbing/Internal/PathHelper.cs index d37baf3..744faec 100644 --- a/Ramstack.Globbing/Internal/PathHelper.cs +++ b/Ramstack.Globbing/Internal/PathHelper.cs @@ -66,16 +66,18 @@ public static bool IsPartialMatch(ReadOnlySpan path, string[] patterns, Ma public static int CountPathSegments(scoped ReadOnlySpan path, MatchFlags flags) { var count = 0; + var iterator = new PathSegmentIterator(); ref var s = ref Unsafe.AsRef(in MemoryMarshal.GetReference(path)); - var iterator = new PathSegmentIterator(path.Length); + var length = path.Length; while (true) { - var r = iterator.GetNext(ref s, flags); + var r = iterator.GetNext(ref s, length, flags); + if (r.start != r.final) count++; - if (r.final == path.Length) + if (r.final == length) break; } @@ -101,17 +103,18 @@ public static ReadOnlySpan GetPartialPattern(string pattern, MatchFlags fl if (depth < 1) depth = 1; + var iterator = new PathSegmentIterator(); ref var s = ref Unsafe.AsRef(in pattern.GetPinnableReference()); - var iterator = new PathSegmentIterator(pattern.Length); + var length = pattern.Length; while (true) { - var r = iterator.GetNext(ref s, flags); + var r = iterator.GetNext(ref s, length, flags); if (r.start != r.final) depth--; if (depth < 1 - || r.final == pattern.Length + || r.final == length || IsGlobStar(ref s, r.start, r.final)) return MemoryMarshal.CreateReadOnlySpan(ref s, r.final); } @@ -197,24 +200,11 @@ static void ConvertPathToPosixStyleImpl(ref char p, nint length) /// private static Vector256 CreateAllowEscaping256Bitmask(MatchFlags flags) { - // Here is a small trick to avoid branching. - // To reduce the number of required instructions, we convert the value `Windows`, - // which equals 2, into a bitmask that allows escaping characters. - // Windows (2) (No character escaping): - // 0000 0010 >> 1 = 0000 0001 - // 0000 0001 & 0000 0001 = 0000 0001 - // 0000 0001 - 1 = 0000 0000 - // Any other value will simply convert to 0. - // Unix (4) (Allow escaping characters) - // 0000 0100 >> 1 = 0000 0010 - // 0000 0010 & 0000 0001 = 0000 0000 - // 0000 0000 - 1 = 1111 1111 - // Next, during the check, we can simply use the Avx2.AndNot instruction instead of Avx2.And: - // Avx2.AndNot( - // allowEscaping, - // Avx2.CompareEqual(chunk, backslash))) - Debug.Assert(MatchFlags.Windows == (MatchFlags)2); - return Vector256.Create(((uint)flags >> 1 & 1) - 1).AsUInt16(); + var mask = Vector256.Zero; + if (flags != MatchFlags.Windows) + mask = Vector256.AllBitsSet; + + return mask; } /// @@ -226,8 +216,11 @@ private static Vector256 CreateAllowEscaping256Bitmask(MatchFlags flags) /// private static Vector128 CreateAllowEscaping128Bitmask(MatchFlags flags) { - Debug.Assert(MatchFlags.Windows == (MatchFlags)2); - return Vector128.Create(((uint)flags >> 1 & 1) - 1).AsUInt16(); + var mask = Vector128.Zero; + if (flags != MatchFlags.Windows) + mask = Vector128.AllBitsSet; + + return mask; } /// @@ -278,23 +271,22 @@ ref Unsafe.As(ref Unsafe.Add(ref destination, offset)), /// private struct PathSegmentIterator { - private nint _last; + private int _last; private nint _position; private uint _mask; - private readonly nint _length; /// /// Initializes a new instance of the structure. /// - /// The path length. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PathSegmentIterator(int length) => - (_last, _length) = (-1, (nint)(uint)length); + public PathSegmentIterator() => + _last = -1; /// /// Retrieves the next segment of the path. /// /// A reference to the starting character of the path. + /// The total number of characters in the input path starting from . /// The flags indicating the type of path separators to match. /// /// A tuple containing the start and end indices of the next path segment. @@ -303,39 +295,49 @@ public PathSegmentIterator(int length) => /// The end of the iteration is indicated by final being equal to the length of the path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int start, int final) GetNext(ref char source, MatchFlags flags) + public (int start, int final) GetNext(ref char source, int length, MatchFlags flags) { - // - // Number of bits per char (ushort) in the MoveMask output - // - const uint BitsPerChar = 0b11; - var start = _last + 1; - while (_position < _length) + while ((int)_position < length) { if ((Avx2.IsSupported || Sse2.IsSupported) && _mask != 0) { var offset = BitOperations.TrailingZeroCount(_mask); - _last = _position + (nint)((uint)offset >> 1); + _last = (int)(_position + (nint)((uint)offset >> 1)); // // Clear the bits for the current separator to process the next position in the mask // - _mask &= ~(BitsPerChar << offset); + _mask &= ~(0b_11u << offset); // // Advance position to the next chunk when no separators remain in the mask // if (_mask == 0) - _position += Avx2.IsSupported + { + // + // https://github.com/dotnet/runtime/issues/117416 + // + // Precompute the stride size instead of calculating it inline + // to avoid stack spilling. For some unknown reason, the JIT + // fails to optimize properly when this is written inline, like so: + // _position += Avx2.IsSupported + // ? Vector256.Count + // : Vector128.Count; + // + + var stride = Avx2.IsSupported ? Vector256.Count : Vector128.Count; - return ((int)start, (int)_last); + _position += stride; + } + + return (start, _last); } - if (Avx2.IsSupported && _position + Vector256.Count <= _length) + if (Avx2.IsSupported && (int)_position + Vector256.Count <= length) { var chunk = LoadVector256(ref source, _position); var allowEscapingMask = CreateAllowEscaping256Bitmask(flags); @@ -362,7 +364,7 @@ public PathSegmentIterator(int length) => if (_mask == 0) _position += Vector256.Count; } - else if (Sse2.IsSupported && !Avx2.IsSupported && _position + Vector128.Count <= _length) + else if (Sse2.IsSupported && !Avx2.IsSupported && (int)_position + Vector128.Count <= length) { var chunk = LoadVector128(ref source, _position); var allowEscapingMask = CreateAllowEscaping128Bitmask(flags); @@ -391,20 +393,21 @@ public PathSegmentIterator(int length) => } else { - for (; _position < _length; _position++) + for (; (int)_position < length; _position++) { var ch = Unsafe.Add(ref source, _position); if (ch == '/' || (ch == '\\' && flags == MatchFlags.Windows)) { - _last = _position; + _last = (int)_position; _position++; - return ((int)start, (int)_last); + + return (start, _last); } } } } - return ((int)start, (int)_length); + return (start, length); } } diff --git a/Ramstack.Globbing/Traversal/FileTreeAsyncEnumerable.cs b/Ramstack.Globbing/Traversal/FileTreeAsyncEnumerable.cs index 1a9bf5d..bb1f977 100644 --- a/Ramstack.Globbing/Traversal/FileTreeAsyncEnumerable.cs +++ b/Ramstack.Globbing/Traversal/FileTreeAsyncEnumerable.cs @@ -84,10 +84,10 @@ IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(Cancellat private async IAsyncEnumerable EnumerateAsync(CancellationTokenSource? source, [EnumeratorCancellation] CancellationToken cancellationToken) { - var chars = ArrayPool.Shared.Rent(512); - try { + var chars = ArrayPool.Shared.Rent(FileTreeEnumerable.DefaultBufferCapacity); + var queue = new Queue<(TEntry Directory, string Path)>(); queue.Enqueue((_directory, "")); @@ -110,10 +110,11 @@ private async IAsyncEnumerable EnumerateAsync(CancellationTokenSource? yield return ResultSelector(entry); } } + + ArrayPool.Shared.Return(chars); } finally { - ArrayPool.Shared.Return(chars); source?.Dispose(); } } diff --git a/Ramstack.Globbing/Traversal/FileTreeEnumerable.cs b/Ramstack.Globbing/Traversal/FileTreeEnumerable.cs index 32b94de..eb907b4 100644 --- a/Ramstack.Globbing/Traversal/FileTreeEnumerable.cs +++ b/Ramstack.Globbing/Traversal/FileTreeEnumerable.cs @@ -14,6 +14,11 @@ public sealed class FileTreeEnumerable : IEnumerable { private readonly TEntry _directory; + /// + /// The default capacity of the character buffer for paths rented from the shared array pool. + /// + internal const int DefaultBufferCapacity = 512; + /// /// Gets or sets the glob patterns to include in the enumeration. /// @@ -72,7 +77,7 @@ IEnumerator IEnumerable.GetEnumerator() => private IEnumerable Enumerate() { - var chars = ArrayPool.Shared.Rent(512); + var chars = ArrayPool.Shared.Rent(DefaultBufferCapacity); var queue = new Queue<(TEntry Directory, string Path)>(); queue.Enqueue((_directory, ""));