From fb0787397fc89908089f5390553429b4e329e479 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 15:55:22 +0500 Subject: [PATCH 1/7] Reduce branch logic to a single MatchFlags check --- Ramstack.Globbing/Matcher.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Ramstack.Globbing/Matcher.cs b/Ramstack.Globbing/Matcher.cs index c872d99..e9976ca 100644 --- a/Ramstack.Globbing/Matcher.cs +++ b/Ramstack.Globbing/Matcher.cs @@ -134,25 +134,34 @@ public static bool IsMatch(string path, string pattern, MatchFlags flags = Match /// public static bool IsMatch(scoped ReadOnlySpan path, scoped ReadOnlySpan pattern, MatchFlags flags = MatchFlags.Auto) { - return IsMatchImpl( + Debug.Assert((int)MatchFlags.Auto == 0); + Debug.Assert((int)MatchFlags.Windows == 2); + Debug.Assert((int)MatchFlags.Unix == 4); + + if (Path.DirectorySeparatorChar == '\\' ? ((int)flags & (int)~MatchFlags.Windows) == 0 : flags == MatchFlags.Windows) + { + return IsMatchImpl( + ref MemoryMarshal.GetReference(path), + path.Length, + ref MemoryMarshal.GetReference(pattern), + pattern.Length); + } + + return IsMatchImpl( ref MemoryMarshal.GetReference(path), path.Length, ref MemoryMarshal.GetReference(pattern), - pattern.Length, - flags); + pattern.Length); [MethodImpl(MethodImplOptions.NoInlining)] - static bool IsMatchImpl(ref char rv, int vlen, ref char rp, int plen, MatchFlags flags) + static bool IsMatchImpl(ref char rv, int vlen, ref char rp, int plen) { fixed (char* v = &rv, p = &rp) { var vend = v + (uint)vlen; var pend = p + (uint)plen; - if (flags == MatchFlags.Windows || flags == MatchFlags.Auto && Path.DirectorySeparatorChar == '\\') - return DoMatch(p, pend, v, vend) == vend; - - return DoMatch(p, pend, v, vend) == vend; + return DoMatch(p, pend, v, vend) == vend; } } From d88a51aa215baf7a5293d3a678f6a093b2dd1ef6 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 17:06:26 +0500 Subject: [PATCH 2/7] Optimize path separator search by replacing Span.IndexOf/IndexOfAny with custom inlineable Vector128 implementation --- Ramstack.Globbing.Tests/MemoryHelperTests.cs | 109 +++++++++++ .../Ramstack.Globbing.Tests.csproj | 2 + Ramstack.Globbing.slnx | 5 +- Ramstack.Globbing/Internal/MemoryHelper.cs | 105 +++++++++++ Ramstack.Globbing/Matcher.cs | 30 +-- .../MatcherBenchmark.cs | 176 ++++++++++++++++++ .../Ramstack.Globbing.Benchmarks/Note.md | 38 ++++ .../Ramstack.Globbing.Benchmarks/Program.cs | 3 + .../Ramstack.Globbing.Benchmarks.csproj | 23 +++ 9 files changed, 468 insertions(+), 23 deletions(-) create mode 100644 Ramstack.Globbing.Tests/MemoryHelperTests.cs create mode 100644 Ramstack.Globbing/Internal/MemoryHelper.cs create mode 100644 benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs create mode 100644 benchmarks/Ramstack.Globbing.Benchmarks/Note.md create mode 100644 benchmarks/Ramstack.Globbing.Benchmarks/Program.cs create mode 100644 benchmarks/Ramstack.Globbing.Benchmarks/Ramstack.Globbing.Benchmarks.csproj diff --git a/Ramstack.Globbing.Tests/MemoryHelperTests.cs b/Ramstack.Globbing.Tests/MemoryHelperTests.cs new file mode 100644 index 0000000..c482662 --- /dev/null +++ b/Ramstack.Globbing.Tests/MemoryHelperTests.cs @@ -0,0 +1,109 @@ +using Ramstack.Globbing.Internal; + +namespace Ramstack.Globbing; + +[TestFixture] +public unsafe class MemoryHelperTests +{ + [Test] + public void IndexOf_Char() + { + Span buffer = stackalloc char[64]; + + while (buffer.Length != 0) + { + Fill(buffer); + + var s = (char*)Unsafe.AsPointer(ref buffer[0]); + Assert.That( + MemoryHelper.IndexOf(s, s + buffer.Length, '*'), + Is.EqualTo(-1)); + + for (var i = 0; i < buffer.Length; i++) + { + Fill(buffer); + buffer[i] = '*'; + + Assert.That( + MemoryHelper.IndexOf(s, s + buffer.Length, '*'), + Is.EqualTo(i)); + } + + buffer = buffer[1..]; + } + } + + [Test] + public void IndexOfAny_CharChar() + { + Span buffer = stackalloc char[64]; + + while (buffer.Length != 0) + { + Fill(buffer); + var s = (char*)Unsafe.AsPointer(ref buffer[0]); + + Assert.That( + MemoryHelper.IndexOfAny(s, s + buffer.Length, '*', '?'), + Is.EqualTo(-1)); + + Assert.That( + MemoryHelper.IndexOfAny(s, s + buffer.Length, '?', '*'), + Is.EqualTo(-1)); + + foreach (var needle in "*?") + { + for (var i = 0; i < buffer.Length; i++) + { + Fill(buffer); + buffer[i] = needle; + + Assert.That( + MemoryHelper.IndexOfAny(s, s + buffer.Length, '*', '?'), + Is.EqualTo(i)); + + Assert.That( + MemoryHelper.IndexOfAny(s, s + buffer.Length, '?', '*'), + Is.EqualTo(i)); + } + } + + if (buffer.Length > 2) + { + foreach (var needle in new[] { "*?", "?*" }) + { + for (var i = 0; i < buffer.Length - 1; i++) + { + Fill(buffer); + buffer[i + 0] = needle[0]; + buffer[i + 1] = needle[1]; + + var r = MemoryHelper.IndexOfAny(s, s + buffer.Length, '*', '?'); + Assert.That(r, Is.EqualTo(i)); + + var p = MemoryHelper.IndexOfAny(s, s + buffer.Length, '?', '*'); + Assert.That(p, Is.EqualTo(i)); + } + } + } + + buffer = buffer[1..]; + } + } + + private static void Fill(Span s) + { + for (var i = 0; i < s.Length; i++) + { + while (true) + { + var v = Random.Shared.Next(32, 127); + if (v is '*' or '?' or '{' or '}' or '[' or '\\') + continue; + + s[i] = (char)v; + break; + } + } + } +} diff --git a/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj b/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj index 2a75b0b..3b1b23f 100644 --- a/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj +++ b/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj @@ -3,11 +3,13 @@ net6.0 enable enable + true preview Ramstack.Globbing + diff --git a/Ramstack.Globbing.slnx b/Ramstack.Globbing.slnx index fba84cf..889a191 100644 --- a/Ramstack.Globbing.slnx +++ b/Ramstack.Globbing.slnx @@ -1,10 +1,13 @@ - + + + + diff --git a/Ramstack.Globbing/Internal/MemoryHelper.cs b/Ramstack.Globbing/Internal/MemoryHelper.cs new file mode 100644 index 0000000..2b0330e --- /dev/null +++ b/Ramstack.Globbing/Internal/MemoryHelper.cs @@ -0,0 +1,105 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace Ramstack.Globbing.Internal; + +internal static unsafe class MemoryHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOf(char* s, char* e, char ch) + { + var i = 0; + + if (Sse2.IsSupported && s + Vector128.Count <= e) + { + for (;;) + { + var result = Sse2.CompareEqual( + Vector128.Create((short)ch), + LoadVector(s)); + + var mask = Sse2.MoveMask(result.AsByte()); + if (mask != 0) + { + var offset = BitOperations.TrailingZeroCount(mask) >>> 1; + return i + offset; + } + + s += Vector128.Count; + i += Vector128.Count; + + if (s + Vector128.Count <= e) + continue; + + if (s == e) + return -1; + + // + // Tail handling via the same SIMD path (no scalar fallback) + // + var remaining = (int)((nint)e - (nint)s) >>> 1; + i = i + remaining - Vector128.Count; + s = e - Vector128.Count; + } + } + + for (; s < e; s++, i++) + if (*s == ch) + return i; + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfAny(char* s, char* e, char ch1, char ch2) + { + var i = 0; + + if (Sse2.IsSupported && s + Vector128.Count <= e) + { + for (;;) + { + var source = LoadVector(s); + var result = Sse2.Or( + Sse2.CompareEqual(source, Vector128.Create((short)ch1)), + Sse2.CompareEqual(source, Vector128.Create((short)ch2)) + ).AsByte(); + + var mask = Sse2.MoveMask(result); + if (mask != 0) + { + var offset = BitOperations.TrailingZeroCount(mask) >>> 1; + return i + offset; + } + + s += Vector128.Count; + i += Vector128.Count; + + if (s + Vector128.Count <= e) + continue; + + if (s == e) + return -1; + + // + // Tail handling via the same SIMD path (no scalar fallback) + // + var remaining = (int)((nint)e - (nint)s) >>> 1; + i = i + remaining - Vector128.Count; + s = e - Vector128.Count; + } + } + + for (; s < e; s++, i++) + if (*s == ch1 || *s == ch2) + return i; + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadVector(void* source) => + Unsafe.ReadUnaligned>(source); +} diff --git a/Ramstack.Globbing/Matcher.cs b/Ramstack.Globbing/Matcher.cs index e9976ca..31ef4f6 100644 --- a/Ramstack.Globbing/Matcher.cs +++ b/Ramstack.Globbing/Matcher.cs @@ -2,6 +2,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Ramstack.Globbing.Internal; + namespace Ramstack.Globbing; /// @@ -165,17 +167,6 @@ static bool IsMatchImpl(ref char rv, int vlen, ref char rp, int plen) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static int Length(char* s, char* e) - { - Debug.Assert((nint)s <= (nint)e); - - // C# emits suboptimal code for the e - s operation in our case. - // However, since the condition s <= e is always true in our case, - // we can assist the JIT in generating efficient code. - return (int)(((nint)e - (nint)s) >>> 1); - } - // Advances the pointer past any slash characters [MethodImpl(MethodImplOptions.AggressiveInlining)] static char* SkipSlash(char* p, char* pend) @@ -189,17 +180,12 @@ static int Length(char* s, char* e) [MethodImpl(MethodImplOptions.AggressiveInlining)] static char* FindNextSlash(char* p, char* pend) { - if (p < pend) - { - var n = Length(p, pend); - var s = MemoryMarshal.CreateSpan(ref *p, n); - var r = typeof(TFlags) == typeof(Windows) - ? s.IndexOfAny('/', '\\') - : s.IndexOf('/'); - - if (r >= 0) - return p + (uint)r; - } + var r = typeof(TFlags) == typeof(Windows) + ? MemoryHelper.IndexOfAny(p, pend, '/', '\\') + : MemoryHelper.IndexOf(p, pend, '/'); + + if (r >= 0) + return p + (uint)r; return pend; } diff --git a/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs b/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs new file mode 100644 index 0000000..ea71805 --- /dev/null +++ b/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs @@ -0,0 +1,176 @@ +namespace Ramstack.Globbing.Benchmarks; + +[OperationsPerSecond] +public class MatcherBenchmark +{ + [Benchmark(Description = "literal: a")] + public bool Literal_1() => + Matcher.IsMatch("a", "a", MatchFlags.Unix); + + // [Benchmark(Description = "literal: src")] + // public bool Literal_2() => + // Matcher.IsMatch("src", "src", MatchFlags.Unix); + // + // [Benchmark(Description = "literal: source")] + // public bool Literal_3() => + // Matcher.IsMatch("source", "source", MatchFlags.Unix); + // + // [Benchmark(Description = "literal: password_generation")] + // public bool Literal_4() => + // Matcher.IsMatch("password_generation", "password_generation", MatchFlags.Unix); + // + // [Benchmark(Description = "star: * -> a")] + // public bool Star_1() => + // Matcher.IsMatch("a", "*", MatchFlags.Unix); + // + // [Benchmark(Description = "star: * -> source")] + // public bool Star_2() => + // Matcher.IsMatch("source", "*", MatchFlags.Unix); + // + // [Benchmark(Description = "star: * -> password_generation")] + // public bool Star_3() => + // Matcher.IsMatch("password_generation", "*", MatchFlags.Unix); + // + // [Benchmark(Description = "star: *generation -> password_generation")] + // public bool Star_4() => + // Matcher.IsMatch("password_generation", "*generation", MatchFlags.Unix); + // + // [Benchmark(Description = "star: password* -> password_generation")] + // public bool Star_5() => + // Matcher.IsMatch("password_generation", "password*", MatchFlags.Unix); + // + // [Benchmark(Description = "star: password*generation -> password_generation")] + // public bool Star_6() => + // Matcher.IsMatch("password_generation", "password*generation", MatchFlags.Unix); + // + // [Benchmark(Description = "star: *.generated.* -> [l=132] XmlComment...verified.cs")] + // public bool Star_7() => + // Matcher.IsMatch( + // "XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs", + // "*.generated.*", + // MatchFlags.Unix); + // + // [Benchmark(Description = "questionmark: ???????????????? -> password_manager")] + // public bool QuestionMarks() => + // Matcher.IsMatch( + // "password_generation", + // "????????????????", + // MatchFlags.Unix); + // + // [Benchmark(Description = "globstar: **/*.java -> [l=199,s=16] /chrome/browser/...ModuleTest.java")] + // public bool Globstar_1() => + // Matcher.IsMatch( + // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + // "**/*.java", + // MatchFlags.Unix); + // + // [Benchmark(Description = "globstar: **/password*/**/*.java -> [l=199,s=16] /chrome/browser/...ModuleTest.java")] + // public bool Globstar_2() => + // Matcher.IsMatch( + // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + // "**/password*/**/*.java", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [aA]...[nN] -> application")] + // public bool CharClass_1() => + // Matcher.IsMatch( + // "application", + // "[aA][pP][pP][lL][iI][cC][aA][tT][iI][oO][nN]", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [Aa]...[Nn] -> application")] + // public bool CharClass_2() => + // Matcher.IsMatch( + // "application", + // "[Aa][Pp][Pp][Ll][Ii][Cc][Aa][Tt][Ii][Oo][Nn]", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [a-z]...[a-z] -> application")] + // public bool CharClass_3() => + // Matcher.IsMatch( + // "application", + // "[a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z]", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [a-zA-Z0-9]...[a-zA-Z0-9] -> application")] + // public bool CharClass_4() => + // Matcher.IsMatch( + // "application", + // "[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [0-9A-Za-z]...[0-9A-Za-z] -> application")] + // public bool CharClass_5() => + // Matcher.IsMatch( + // "application", + // "[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [a-zA-Z0-9]pplication -> application")] + // public bool CharClass_6() => + // Matcher.IsMatch( + // "application", + // "[a-zA-Z0-9]pplication", + // MatchFlags.Unix); + // + // [Benchmark(Description = "charclass: [0-9A-Za-z]pplication -> application")] + // public bool CharClass_7() => + // Matcher.IsMatch("application", "[0-9A-Za-z]pplication", MatchFlags.Unix); + // + // [Benchmark(Description = "pattern: *.jpg -> [l=54] sunset...1920x1080.jpg")] + // public bool Pattern_1() => + // Matcher.IsMatch( + // "sunset_over_mountain_lake_20260212153346_1920x1080.jpg", + // "*.jpg", + // MatchFlags.Unix); + // + // [Benchmark(Description = "pattern: *.cs -> [l=132] XmlComment...verified.cs")] + // public bool Pattern_2() => + // Matcher.IsMatch( + // "XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs", + // "*.cs", + // MatchFlags.Unix); + // + // [Benchmark(Description = "brace: *.{jpg,png} -> [l=54] sunset...1920x1080.jpg")] + // public bool Braces_1() => + // Matcher.IsMatch( + // "sunset_over_mountain_lake_20260212153346_1920x1080.jpg", + // "*.{jpg,png}", + // MatchFlags.Unix); + // + // [Benchmark(Description = "brace: *.{jpg,png,gif,webp} -> [l=54] sunset...1920x1080.jpg")] + // public bool Braces_2() => + // Matcher.IsMatch("sunset_over_mountain_lake_20260212153346_1920x1080.webp", + // "*.{jpg,png,gif,webp}", + // MatchFlags.Unix); + // + // [Benchmark(Description = "path: 0/1/2/3 -> 0/1/2/3")] + // public bool Path_1() => + // Matcher.IsMatch("0/1/2/3", "0/1/2/3", MatchFlags.Unix); + // + // [Benchmark(Description = "path: 0000/1111/2222/3333/4444 -> 0000/.../4444")] + // public bool Path_2() => + // Matcher.IsMatch( + // "0000/1111/2222/3333/4444", + // "0000/1111/2222/3333/4444", + // MatchFlags.Unix); + // + // [Benchmark(Description = "path, star: */*/*/*/*/*/*/* -> 1/2/3/4/5/6/7/8")] + // public bool Path_Star_1() => + // Matcher.IsMatch("1/2/3/4/5/6/7/8", "*/*/*/*/*/*/*/*", MatchFlags.Unix); + // + // [Benchmark(Description = + // "path, star: [s=16] */*/.../*/* -> /chrome/.../TouchToFillPasswordGenerationModuleTest.java")] + // public bool Path_Star_2() => + // Matcher.IsMatch( + // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + // "*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*", + // MatchFlags.Unix); + // + // [Benchmark(Description = "path, literal: [l=199] /chrome/...ModuleTest.java -> /chrome/...ModuleTest.java")] + // public bool Path_Literal_1() => + // Matcher.IsMatch( + // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + // MatchFlags.Unix); +} diff --git a/benchmarks/Ramstack.Globbing.Benchmarks/Note.md b/benchmarks/Ramstack.Globbing.Benchmarks/Note.md new file mode 100644 index 0000000..4b7533a --- /dev/null +++ b/benchmarks/Ramstack.Globbing.Benchmarks/Note.md @@ -0,0 +1,38 @@ +BenchmarkDotNet v0.15.8, Linux CachyOS +AMD Ryzen 9 5900X 1.73GHz, 1 CPU, 24 logical and 12 physical cores +.NET SDK 10.0.104 +[Host] : .NET 10.0.4 (10.0.4, 42.42.42.42424), X64 RyuJIT x86-64-v3 +DefaultJob : .NET 10.0.4 (10.0.4, 42.42.42.42424), X64 RyuJIT x86-64-v3 + +| Group | Pattern | Value | Mean | Op/s | IndexOf | IndexOf | +|---------------|---------------------------------|-------------------------------------------------|--------:|--------------:|----------:|----------------:| +| literal | a | a | 7.026 | 142,321,725.3 | ✔ 5.590 | ✔ 178,894,106.5 | +| literal | src | src | 9.506 | 105,195,155.3 | ✔ 7.629 | ✔ 131,072,276.4 | +| literal | source | source | 11.963 | 83,593,772.6 | 11.347 | 88,130,073.7 | +| literal | password_generation | password_generation | 20.503 | 48,773,405.2 | 22.233 | 44,977,609.2 | +| star | * | a | 7.074 | 141,355,739.2 | ✔ 4.764 | ✔ 209,926,476.5 | +| star | * | source | 7.046 | 141,926,616.7 | ✔ 6.191 | ✔ 161,527,242.5 | +| star | * | password_generation | 6.855 | 145,875,161.3 | ✔ 6.225 | ✔ 160,634,650.2 | +| star | *generation | password_generation | 38.473 | 25,992,511.4 | 38.518 | 25,962,122.2 | +| star | password* | password_generation | 13.540 | 73,856,975.0 | 12.637 | 79,131,453.3 | +| star | password*generation | password_generation | 25.981 | 38,489,492.9 | 26.557 | 37,655,098.6 | +| star | *.generated.* | [l=132] XmlComment...verified.cs | 327.465 | 3,053,764.0 | 305.625 | 3,271,985.2 | +| questionmark | ???????????????? | password_manager | 16.325 | 61,254,206.3 | 15.474 | 64,623,403.7 | +| globstar | **/*.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 634.790 | 1,575,323.4 | 582.265 | 1,717,430.7 | +| globstar | **/password*/**/*.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 521.348 | 1,918,106.3 | 472.446 | 2,116,643.6 | +| charclass | [aA]...[nN] | application | 32.289 | 30,970,506.7 | 29.776 | 33,583,921.8 | +| charclass | [Aa]...[Nn] | application | 32.014 | 31,236,032.6 | 29.820 | 33,534,043.7 | +| charclass | [a-z]...[a-z] | application | 25.671 | 38,953,918.4 | 26.024 | 38,426,781.7 | +| charclass | [a-zA-Z0-9]...[a-zA-Z0-9] | application | 48.751 | 20,512,333.4 | 52.198 | 19,157,910.1 | +| charclass | [0-9A-Za-z]...[0-9A-Za-z] | application | 49.074 | 20,377,481.8 | 52.476 | 19,056,365.1 | +| charclass | [a-zA-Z0-9]pplication | application | 18.960 | 52,742,644.8 | 17.231 | 58,034,423.0 | +| charclass | [0-9A-Za-z]pplication | application | 19.015 | 52,589,136.4 | 18.513 | 54,017,011.0 | +| pattern | *.jpg | [l=54] sunset...1920x1080.jpg | 137.728 | 7,260,668.7 | 136.722 | 7,314,120.9 | +| pattern | *.cs | [l=132] XmlComment...verified.cs | 367.438 | 2,721,548.9 | 341.344 | 2,929,598.9 | +| brace | *.{jpg,png} | [l=54] sunset...1920x1080.jpg | 154.785 | 6,460,580.0 | 155.439 | 6,433,391.6 | +| brace | *.{jpg,png,gif,webp} | [l=54] sunset...1920x1080.jpg | 169.206 | 5,909,952.3 | 169.354 | 5,904,782.4 | +| path | 0/1/2/3 | 0/1/2/3 | 23.348 | 42,830,676.1 | ✔ 17.064 | ✔ 58,604,216.9 | +| path | 0000/1111/2222/3333/4444 | 0000/1111/2222/3333/4444 | 43.299 | 23,095,357.4 | ✔ 32.740 | ✔ 30,543,364.7 | +| path, star | */*/*/*/*/*/*/* | 1/2/3/4/5/6/7/8 | 44.458 | 22,493,287.7 | ✔ 29.100 | ✔ 34,364,201.0 | +| path, star | */*/*/*/*/*/*/*/*/*/*/*/*/*/*/* | [l=199,s=16] /chrome/browser/...ModuleTest.java | 97.207 | 10,287,286.8 | ✔ 61.376 | ✔ 16,293,119.6 | +| path, literal | /chrome/...ModuleTest.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 249.692 | 4,004,931.1 | ✔ 214.788 | ✔ 4,655,746.6 | diff --git a/benchmarks/Ramstack.Globbing.Benchmarks/Program.cs b/benchmarks/Ramstack.Globbing.Benchmarks/Program.cs new file mode 100644 index 0000000..34b8cae --- /dev/null +++ b/benchmarks/Ramstack.Globbing.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using Ramstack.Globbing.Benchmarks; + +BenchmarkRunner.Run(); diff --git a/benchmarks/Ramstack.Globbing.Benchmarks/Ramstack.Globbing.Benchmarks.csproj b/benchmarks/Ramstack.Globbing.Benchmarks/Ramstack.Globbing.Benchmarks.csproj new file mode 100644 index 0000000..1788a90 --- /dev/null +++ b/benchmarks/Ramstack.Globbing.Benchmarks/Ramstack.Globbing.Benchmarks.csproj @@ -0,0 +1,23 @@ + + + Exe + net10.0 + enable + enable + true + + + + + + + + + + + + + + + + From 0a48806cc253966ec9d38f3754d7f785bd372504 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 17:09:59 +0500 Subject: [PATCH 3/7] Ignore BenchmarkDotNet.Artifacts folder --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 4f81b1b..88e147a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ obj/ #Ignore Rider/Idea files .idea/ + +# Ignore BenchmarkDotNet artifacts +BenchmarkDotNet.Artifacts From 5cdce3aa47d77b8e1b9885092ad0356c9b7e1463 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 17:10:14 +0500 Subject: [PATCH 4/7] Formatting --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 88e147a..294b8a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -#Ignore thumbnails created by windows +# Ignore thumbnails created by windows [Tt]humbs.db -#Ignore files build by Visual Studio +# Ignore files build by Visual Studio *.obj *.exe *.pdb @@ -33,7 +33,7 @@ obj/ *.ReSharper.user [Tt]est[Rr]esult* -#Ignore Rider/Idea files +# Ignore Rider/Idea files .idea/ # Ignore BenchmarkDotNet artifacts From e0cd07dfd7c1080ced2ab3f9b39bb66f7a7f1aa8 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 17:20:34 +0500 Subject: [PATCH 5/7] Uncomment benchmark tests --- .../MatcherBenchmark.cs | 332 +++++++++--------- 1 file changed, 166 insertions(+), 166 deletions(-) diff --git a/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs b/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs index ea71805..0f5566c 100644 --- a/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs +++ b/benchmarks/Ramstack.Globbing.Benchmarks/MatcherBenchmark.cs @@ -7,170 +7,170 @@ public class MatcherBenchmark public bool Literal_1() => Matcher.IsMatch("a", "a", MatchFlags.Unix); - // [Benchmark(Description = "literal: src")] - // public bool Literal_2() => - // Matcher.IsMatch("src", "src", MatchFlags.Unix); - // - // [Benchmark(Description = "literal: source")] - // public bool Literal_3() => - // Matcher.IsMatch("source", "source", MatchFlags.Unix); - // - // [Benchmark(Description = "literal: password_generation")] - // public bool Literal_4() => - // Matcher.IsMatch("password_generation", "password_generation", MatchFlags.Unix); - // - // [Benchmark(Description = "star: * -> a")] - // public bool Star_1() => - // Matcher.IsMatch("a", "*", MatchFlags.Unix); - // - // [Benchmark(Description = "star: * -> source")] - // public bool Star_2() => - // Matcher.IsMatch("source", "*", MatchFlags.Unix); - // - // [Benchmark(Description = "star: * -> password_generation")] - // public bool Star_3() => - // Matcher.IsMatch("password_generation", "*", MatchFlags.Unix); - // - // [Benchmark(Description = "star: *generation -> password_generation")] - // public bool Star_4() => - // Matcher.IsMatch("password_generation", "*generation", MatchFlags.Unix); - // - // [Benchmark(Description = "star: password* -> password_generation")] - // public bool Star_5() => - // Matcher.IsMatch("password_generation", "password*", MatchFlags.Unix); - // - // [Benchmark(Description = "star: password*generation -> password_generation")] - // public bool Star_6() => - // Matcher.IsMatch("password_generation", "password*generation", MatchFlags.Unix); - // - // [Benchmark(Description = "star: *.generated.* -> [l=132] XmlComment...verified.cs")] - // public bool Star_7() => - // Matcher.IsMatch( - // "XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs", - // "*.generated.*", - // MatchFlags.Unix); - // - // [Benchmark(Description = "questionmark: ???????????????? -> password_manager")] - // public bool QuestionMarks() => - // Matcher.IsMatch( - // "password_generation", - // "????????????????", - // MatchFlags.Unix); - // - // [Benchmark(Description = "globstar: **/*.java -> [l=199,s=16] /chrome/browser/...ModuleTest.java")] - // public bool Globstar_1() => - // Matcher.IsMatch( - // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", - // "**/*.java", - // MatchFlags.Unix); - // - // [Benchmark(Description = "globstar: **/password*/**/*.java -> [l=199,s=16] /chrome/browser/...ModuleTest.java")] - // public bool Globstar_2() => - // Matcher.IsMatch( - // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", - // "**/password*/**/*.java", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [aA]...[nN] -> application")] - // public bool CharClass_1() => - // Matcher.IsMatch( - // "application", - // "[aA][pP][pP][lL][iI][cC][aA][tT][iI][oO][nN]", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [Aa]...[Nn] -> application")] - // public bool CharClass_2() => - // Matcher.IsMatch( - // "application", - // "[Aa][Pp][Pp][Ll][Ii][Cc][Aa][Tt][Ii][Oo][Nn]", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [a-z]...[a-z] -> application")] - // public bool CharClass_3() => - // Matcher.IsMatch( - // "application", - // "[a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z]", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [a-zA-Z0-9]...[a-zA-Z0-9] -> application")] - // public bool CharClass_4() => - // Matcher.IsMatch( - // "application", - // "[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [0-9A-Za-z]...[0-9A-Za-z] -> application")] - // public bool CharClass_5() => - // Matcher.IsMatch( - // "application", - // "[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [a-zA-Z0-9]pplication -> application")] - // public bool CharClass_6() => - // Matcher.IsMatch( - // "application", - // "[a-zA-Z0-9]pplication", - // MatchFlags.Unix); - // - // [Benchmark(Description = "charclass: [0-9A-Za-z]pplication -> application")] - // public bool CharClass_7() => - // Matcher.IsMatch("application", "[0-9A-Za-z]pplication", MatchFlags.Unix); - // - // [Benchmark(Description = "pattern: *.jpg -> [l=54] sunset...1920x1080.jpg")] - // public bool Pattern_1() => - // Matcher.IsMatch( - // "sunset_over_mountain_lake_20260212153346_1920x1080.jpg", - // "*.jpg", - // MatchFlags.Unix); - // - // [Benchmark(Description = "pattern: *.cs -> [l=132] XmlComment...verified.cs")] - // public bool Pattern_2() => - // Matcher.IsMatch( - // "XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs", - // "*.cs", - // MatchFlags.Unix); - // - // [Benchmark(Description = "brace: *.{jpg,png} -> [l=54] sunset...1920x1080.jpg")] - // public bool Braces_1() => - // Matcher.IsMatch( - // "sunset_over_mountain_lake_20260212153346_1920x1080.jpg", - // "*.{jpg,png}", - // MatchFlags.Unix); - // - // [Benchmark(Description = "brace: *.{jpg,png,gif,webp} -> [l=54] sunset...1920x1080.jpg")] - // public bool Braces_2() => - // Matcher.IsMatch("sunset_over_mountain_lake_20260212153346_1920x1080.webp", - // "*.{jpg,png,gif,webp}", - // MatchFlags.Unix); - // - // [Benchmark(Description = "path: 0/1/2/3 -> 0/1/2/3")] - // public bool Path_1() => - // Matcher.IsMatch("0/1/2/3", "0/1/2/3", MatchFlags.Unix); - // - // [Benchmark(Description = "path: 0000/1111/2222/3333/4444 -> 0000/.../4444")] - // public bool Path_2() => - // Matcher.IsMatch( - // "0000/1111/2222/3333/4444", - // "0000/1111/2222/3333/4444", - // MatchFlags.Unix); - // - // [Benchmark(Description = "path, star: */*/*/*/*/*/*/* -> 1/2/3/4/5/6/7/8")] - // public bool Path_Star_1() => - // Matcher.IsMatch("1/2/3/4/5/6/7/8", "*/*/*/*/*/*/*/*", MatchFlags.Unix); - // - // [Benchmark(Description = - // "path, star: [s=16] */*/.../*/* -> /chrome/.../TouchToFillPasswordGenerationModuleTest.java")] - // public bool Path_Star_2() => - // Matcher.IsMatch( - // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", - // "*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*", - // MatchFlags.Unix); - // - // [Benchmark(Description = "path, literal: [l=199] /chrome/...ModuleTest.java -> /chrome/...ModuleTest.java")] - // public bool Path_Literal_1() => - // Matcher.IsMatch( - // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", - // "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", - // MatchFlags.Unix); + [Benchmark(Description = "literal: src")] + public bool Literal_2() => + Matcher.IsMatch("src", "src", MatchFlags.Unix); + + [Benchmark(Description = "literal: source")] + public bool Literal_3() => + Matcher.IsMatch("source", "source", MatchFlags.Unix); + + [Benchmark(Description = "literal: password_generation")] + public bool Literal_4() => + Matcher.IsMatch("password_generation", "password_generation", MatchFlags.Unix); + + [Benchmark(Description = "star: * -> a")] + public bool Star_1() => + Matcher.IsMatch("a", "*", MatchFlags.Unix); + + [Benchmark(Description = "star: * -> source")] + public bool Star_2() => + Matcher.IsMatch("source", "*", MatchFlags.Unix); + + [Benchmark(Description = "star: * -> password_generation")] + public bool Star_3() => + Matcher.IsMatch("password_generation", "*", MatchFlags.Unix); + + [Benchmark(Description = "star: *generation -> password_generation")] + public bool Star_4() => + Matcher.IsMatch("password_generation", "*generation", MatchFlags.Unix); + + [Benchmark(Description = "star: password* -> password_generation")] + public bool Star_5() => + Matcher.IsMatch("password_generation", "password*", MatchFlags.Unix); + + [Benchmark(Description = "star: password*generation -> password_generation")] + public bool Star_6() => + Matcher.IsMatch("password_generation", "password*generation", MatchFlags.Unix); + + [Benchmark(Description = "star: *.generated.* -> [l=132] XmlComment...verified.cs")] + public bool Star_7() => + Matcher.IsMatch( + "XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs", + "*.generated.*", + MatchFlags.Unix); + + [Benchmark(Description = "questionmark: ???????????????? -> password_manager")] + public bool QuestionMarks() => + Matcher.IsMatch( + "password_generation", + "????????????????", + MatchFlags.Unix); + + [Benchmark(Description = "globstar: **/*.java -> [l=199,s=16] /chrome/browser/...ModuleTest.java")] + public bool Globstar_1() => + Matcher.IsMatch( + "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + "**/*.java", + MatchFlags.Unix); + + [Benchmark(Description = "globstar: **/password*/**/*.java -> [l=199,s=16] /chrome/browser/...ModuleTest.java")] + public bool Globstar_2() => + Matcher.IsMatch( + "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + "**/password*/**/*.java", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [aA]...[nN] -> application")] + public bool CharClass_1() => + Matcher.IsMatch( + "application", + "[aA][pP][pP][lL][iI][cC][aA][tT][iI][oO][nN]", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [Aa]...[Nn] -> application")] + public bool CharClass_2() => + Matcher.IsMatch( + "application", + "[Aa][Pp][Pp][Ll][Ii][Cc][Aa][Tt][Ii][Oo][Nn]", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [a-z]...[a-z] -> application")] + public bool CharClass_3() => + Matcher.IsMatch( + "application", + "[a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z][a-z]", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [a-zA-Z0-9]...[a-zA-Z0-9] -> application")] + public bool CharClass_4() => + Matcher.IsMatch( + "application", + "[a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [0-9A-Za-z]...[0-9A-Za-z] -> application")] + public bool CharClass_5() => + Matcher.IsMatch( + "application", + "[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [a-zA-Z0-9]pplication -> application")] + public bool CharClass_6() => + Matcher.IsMatch( + "application", + "[a-zA-Z0-9]pplication", + MatchFlags.Unix); + + [Benchmark(Description = "charclass: [0-9A-Za-z]pplication -> application")] + public bool CharClass_7() => + Matcher.IsMatch("application", "[0-9A-Za-z]pplication", MatchFlags.Unix); + + [Benchmark(Description = "pattern: *.jpg -> [l=54] sunset...1920x1080.jpg")] + public bool Pattern_1() => + Matcher.IsMatch( + "sunset_over_mountain_lake_20260212153346_1920x1080.jpg", + "*.jpg", + MatchFlags.Unix); + + [Benchmark(Description = "pattern: *.cs -> [l=132] XmlComment...verified.cs")] + public bool Pattern_2() => + Matcher.IsMatch( + "XmlCommentDocumentationIdTests.CanMergeXmlCommentsWithDifferentDocumentationIdFormats#OpenApiXmlCommentSupport.generated.verified.cs", + "*.cs", + MatchFlags.Unix); + + [Benchmark(Description = "brace: *.{jpg,png} -> [l=54] sunset...1920x1080.jpg")] + public bool Braces_1() => + Matcher.IsMatch( + "sunset_over_mountain_lake_20260212153346_1920x1080.jpg", + "*.{jpg,png}", + MatchFlags.Unix); + + [Benchmark(Description = "brace: *.{jpg,png,gif,webp} -> [l=54] sunset...1920x1080.jpg")] + public bool Braces_2() => + Matcher.IsMatch("sunset_over_mountain_lake_20260212153346_1920x1080.webp", + "*.{jpg,png,gif,webp}", + MatchFlags.Unix); + + [Benchmark(Description = "path: 0/1/2/3 -> 0/1/2/3")] + public bool Path_1() => + Matcher.IsMatch("0/1/2/3", "0/1/2/3", MatchFlags.Unix); + + [Benchmark(Description = "path: 0000/1111/2222/3333/4444 -> 0000/.../4444")] + public bool Path_2() => + Matcher.IsMatch( + "0000/1111/2222/3333/4444", + "0000/1111/2222/3333/4444", + MatchFlags.Unix); + + [Benchmark(Description = "path, star: */*/*/*/*/*/*/* -> 1/2/3/4/5/6/7/8")] + public bool Path_Star_1() => + Matcher.IsMatch("1/2/3/4/5/6/7/8", "*/*/*/*/*/*/*/*", MatchFlags.Unix); + + [Benchmark(Description = + "path, star: [s=16] */*/.../*/* -> /chrome/.../TouchToFillPasswordGenerationModuleTest.java")] + public bool Path_Star_2() => + Matcher.IsMatch( + "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + "*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*", + MatchFlags.Unix); + + [Benchmark(Description = "path, literal: [l=199] /chrome/...ModuleTest.java -> /chrome/...ModuleTest.java")] + public bool Path_Literal_1() => + Matcher.IsMatch( + "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + "/chrome/browser/touch_to_fill/password_manager/password_generation/android/internal/java/src/org/chromium/chrome/browser/touch_to_fill/password_generation/TouchToFillPasswordGenerationModuleTest.java", + MatchFlags.Unix); } From 009c7400033a25de1528db825d1131779c68fc73 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 17:55:48 +0500 Subject: [PATCH 6/7] Reduce wildcard backtracking using literal anchors --- Ramstack.Globbing/Matcher.cs | 38 ++++++++++- .../Ramstack.Globbing.Benchmarks/Note.md | 64 +++++++++---------- 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/Ramstack.Globbing/Matcher.cs b/Ramstack.Globbing/Matcher.cs index 31ef4f6..bd17a5d 100644 --- a/Ramstack.Globbing/Matcher.cs +++ b/Ramstack.Globbing/Matcher.cs @@ -134,6 +134,7 @@ public static bool IsMatch(string path, string pattern, MatchFlags flags = Match /// /// if the pattern matches the path; otherwise, . /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsMatch(scoped ReadOnlySpan path, scoped ReadOnlySpan pattern, MatchFlags flags = MatchFlags.Auto) { Debug.Assert((int)MatchFlags.Auto == 0); @@ -361,8 +362,42 @@ static bool IsMatchImpl(ref char rv, int vlen, ref char rp, int plen) if (p == pend) return vend; + // OPTIMIZATION: + // Try to identify a fast-forward opportunity after '*' by inspecting the next pattern character. + // If the next character is a plain literal (not a wildcard or special construct), + // we can skip naive backtracking and jump directly to its next occurrence in the input. + // + // This reduces the number of recursive calls by skipping intermediate positions + // that cannot possibly match, effectively replacing blind linear backtracking + // with a guided search. + // + // For non-literal tokens ('?', '[', '{', '\'), we fall back to the original behavior + // to preserve correctness. + + // Note: '*' is intentionally not included here, as consecutive '*' are collapsed above, + // so the next pattern character is guaranteed not to be '*'. + const long Mask = + 1L << ('?' - 63) | // 63 + 1L << ('[' - 63) | // 91 + 1L << ('\\' - 63) | // 92 + 1L << ('{' - 63); // 123 + + var lookup = p[0] - 63; + while (true) { + if ((uint)lookup > 60 || ((1L << lookup) & Mask) == 0) + { + var n = MemoryHelper.IndexOf(v, vend, p[0]); + if (n < 0) + { + v = vend; + break; + } + + v += (uint)n; + } + var r = DoMatchSegment(p, pend, v, vend, subpattern); if (r != null) return r; @@ -371,6 +406,7 @@ static bool IsMatchImpl(ref char rv, int vlen, ref char rp, int plen) break; } + // OPTIMIZATION: // Aborting recursion when failing // // To prevent quadratic behavior in scenarios like the pattern "a*a*a*a*c" @@ -399,7 +435,7 @@ static bool IsMatchImpl(ref char rv, int vlen, ref char rp, int plen) default: { - if (p[0] != '?' && p[0] != v[0]) + if (p[0] != v[0] && p[0] != '?') return null; p++; diff --git a/benchmarks/Ramstack.Globbing.Benchmarks/Note.md b/benchmarks/Ramstack.Globbing.Benchmarks/Note.md index 4b7533a..1ecc712 100644 --- a/benchmarks/Ramstack.Globbing.Benchmarks/Note.md +++ b/benchmarks/Ramstack.Globbing.Benchmarks/Note.md @@ -4,35 +4,35 @@ AMD Ryzen 9 5900X 1.73GHz, 1 CPU, 24 logical and 12 physical cores [Host] : .NET 10.0.4 (10.0.4, 42.42.42.42424), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.4 (10.0.4, 42.42.42.42424), X64 RyuJIT x86-64-v3 -| Group | Pattern | Value | Mean | Op/s | IndexOf | IndexOf | -|---------------|---------------------------------|-------------------------------------------------|--------:|--------------:|----------:|----------------:| -| literal | a | a | 7.026 | 142,321,725.3 | ✔ 5.590 | ✔ 178,894,106.5 | -| literal | src | src | 9.506 | 105,195,155.3 | ✔ 7.629 | ✔ 131,072,276.4 | -| literal | source | source | 11.963 | 83,593,772.6 | 11.347 | 88,130,073.7 | -| literal | password_generation | password_generation | 20.503 | 48,773,405.2 | 22.233 | 44,977,609.2 | -| star | * | a | 7.074 | 141,355,739.2 | ✔ 4.764 | ✔ 209,926,476.5 | -| star | * | source | 7.046 | 141,926,616.7 | ✔ 6.191 | ✔ 161,527,242.5 | -| star | * | password_generation | 6.855 | 145,875,161.3 | ✔ 6.225 | ✔ 160,634,650.2 | -| star | *generation | password_generation | 38.473 | 25,992,511.4 | 38.518 | 25,962,122.2 | -| star | password* | password_generation | 13.540 | 73,856,975.0 | 12.637 | 79,131,453.3 | -| star | password*generation | password_generation | 25.981 | 38,489,492.9 | 26.557 | 37,655,098.6 | -| star | *.generated.* | [l=132] XmlComment...verified.cs | 327.465 | 3,053,764.0 | 305.625 | 3,271,985.2 | -| questionmark | ???????????????? | password_manager | 16.325 | 61,254,206.3 | 15.474 | 64,623,403.7 | -| globstar | **/*.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 634.790 | 1,575,323.4 | 582.265 | 1,717,430.7 | -| globstar | **/password*/**/*.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 521.348 | 1,918,106.3 | 472.446 | 2,116,643.6 | -| charclass | [aA]...[nN] | application | 32.289 | 30,970,506.7 | 29.776 | 33,583,921.8 | -| charclass | [Aa]...[Nn] | application | 32.014 | 31,236,032.6 | 29.820 | 33,534,043.7 | -| charclass | [a-z]...[a-z] | application | 25.671 | 38,953,918.4 | 26.024 | 38,426,781.7 | -| charclass | [a-zA-Z0-9]...[a-zA-Z0-9] | application | 48.751 | 20,512,333.4 | 52.198 | 19,157,910.1 | -| charclass | [0-9A-Za-z]...[0-9A-Za-z] | application | 49.074 | 20,377,481.8 | 52.476 | 19,056,365.1 | -| charclass | [a-zA-Z0-9]pplication | application | 18.960 | 52,742,644.8 | 17.231 | 58,034,423.0 | -| charclass | [0-9A-Za-z]pplication | application | 19.015 | 52,589,136.4 | 18.513 | 54,017,011.0 | -| pattern | *.jpg | [l=54] sunset...1920x1080.jpg | 137.728 | 7,260,668.7 | 136.722 | 7,314,120.9 | -| pattern | *.cs | [l=132] XmlComment...verified.cs | 367.438 | 2,721,548.9 | 341.344 | 2,929,598.9 | -| brace | *.{jpg,png} | [l=54] sunset...1920x1080.jpg | 154.785 | 6,460,580.0 | 155.439 | 6,433,391.6 | -| brace | *.{jpg,png,gif,webp} | [l=54] sunset...1920x1080.jpg | 169.206 | 5,909,952.3 | 169.354 | 5,904,782.4 | -| path | 0/1/2/3 | 0/1/2/3 | 23.348 | 42,830,676.1 | ✔ 17.064 | ✔ 58,604,216.9 | -| path | 0000/1111/2222/3333/4444 | 0000/1111/2222/3333/4444 | 43.299 | 23,095,357.4 | ✔ 32.740 | ✔ 30,543,364.7 | -| path, star | */*/*/*/*/*/*/* | 1/2/3/4/5/6/7/8 | 44.458 | 22,493,287.7 | ✔ 29.100 | ✔ 34,364,201.0 | -| path, star | */*/*/*/*/*/*/*/*/*/*/*/*/*/*/* | [l=199,s=16] /chrome/browser/...ModuleTest.java | 97.207 | 10,287,286.8 | ✔ 61.376 | ✔ 16,293,119.6 | -| path, literal | /chrome/...ModuleTest.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 249.692 | 4,004,931.1 | ✔ 214.788 | ✔ 4,655,746.6 | +| Group | Pattern | Value | Mean | Op/s | IndexOf | IndexOf | Star anchor | Star anchor | +|---------------|---------------------------------|-------------------------------------------------|-----------:|--------------:|-------------:|----------------:|--------------:|----------------:| +| literal | a | a | 7.026 ns | 142,321,725.3 | ✔ 5.590 ns | ✔ 178,894,106.5 | ✔ 5.284 ns | ✔ 189,234,700.8 | +| literal | src | src | 9.506 ns | 105,195,155.3 | ✔ 7.629 ns | ✔ 131,072,276.4 | 7.593 ns | 131,700,505.2 | +| literal | source | source | 11.963 ns | 83,593,772.6 | 11.347 ns | 88,130,073.7 | 10.672 ns | 93,699,657.8 | +| literal | password_generation | password_generation | 20.503 ns | 48,773,405.2 | 22.233 ns | 44,977,609.2 | 18.727 ns | 53,398,971.7 | +| star | * | a | 7.074 ns | 141,355,739.2 | ✔ 4.764 ns | ✔ 209,926,476.5 | ✔ 4.745 ns | ✔ 210,735,952.3 | +| star | * | source | 7.046 ns | 141,926,616.7 | ✔ 6.191 ns | ✔ 161,527,242.5 | ✔ 5.770 ns | ✔ 173,322,670.8 | +| star | * | password_generation | 6.855 ns | 145,875,161.3 | ✔ 6.225 ns | ✔ 160,634,650.2 | ✔ 6.073 ns | ✔ 164,674,416.2 | +| star | *generation | password_generation | 38.473 ns | 25,992,511.4 | 38.518 ns | 25,962,122.2 | ✔ 16.407 ns | ✔ 60,950,103.6 | +| star | password* | password_generation | 13.540 ns | 73,856,975.0 | 12.637 ns | 79,131,453.3 | ✔ 10.430 ns | ✔ 95,879,133.7 | +| star | password*generation | password_generation | 25.981 ns | 38,489,492.9 | 26.557 ns | 37,655,098.6 | ✔ 21.182 ns | ✔ 47,210,373.7 | +| star | *.generated.* | [l=132] XmlComment...verified.cs | 327.465 ns | 3,053,764.0 | 305.625 ns | 3,271,985.2 | ✔ 28.391 ns | ✔ 35,221,923.8 | +| questionmark | ???????????????? | password_manager | 16.325 ns | 61,254,206.3 | 15.474 ns | 64,623,403.7 | 16.829 ns | 59,421,836.8 | +| globstar | **/*.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 634.790 ns | 1,575,323.4 | 582.265 ns | 1,717,430.7 | ✔ 148.568 ns | ✔ 6,730,904.4 | +| globstar | **/password*/**/*.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 521.348 ns | 1,918,106.3 | 472.446 ns | 2,116,643.6 | ✔ 147.048 ns | ✔ 6,800,506.0 | +| charclass | [aA]...[nN] | application | 32.289 ns | 30,970,506.7 | 29.776 ns | 33,583,921.8 | 29.927 ns | 33,414,321.4 | +| charclass | [Aa]...[Nn] | application | 32.014 ns | 31,236,032.6 | 29.820 ns | 33,534,043.7 | 29.805 ns | 33,551,273.5 | +| charclass | [a-z]...[a-z] | application | 25.671 ns | 38,953,918.4 | 26.024 ns | 38,426,781.7 | 25.886 ns | 38,631,517.3 | +| charclass | [a-zA-Z0-9]...[a-zA-Z0-9] | application | 48.751 ns | 20,512,333.4 | 52.198 ns | 19,157,910.1 | 51.962 ns | 19,244,905.1 | +| charclass | [0-9A-Za-z]...[0-9A-Za-z] | application | 49.074 ns | 20,377,481.8 | 52.476 ns | 19,056,365.1 | 52.461 ns | 19,061,895.8 | +| charclass | [a-zA-Z0-9]pplication | application | 18.960 ns | 52,742,644.8 | 17.231 ns | 58,034,423.0 | 16.151 ns | 61,915,495.3 | +| charclass | [0-9A-Za-z]pplication | application | 19.015 ns | 52,589,136.4 | 18.513 ns | 54,017,011.0 | 16.117 ns | 62,044,465.8 | +| pattern | *.jpg | [l=54] sunset...1920x1080.jpg | 137.728 ns | 7,260,668.7 | 136.722 ns | 7,314,120.9 | ✔ 15.007 ns | ✔ 66,637,283.8 | +| pattern | *.cs | [l=132] XmlComment...verified.cs | 367.438 ns | 2,721,548.9 | 341.344 ns | 2,929,598.9 | ✔ 31.967 ns | ✔ 31,282,078.9 | +| brace | *.{jpg,png} | [l=54] sunset...1920x1080.jpg | 154.785 ns | 6,460,580.0 | 155.439 ns | 6,433,391.6 | ✔ 26.122 ns | ✔ 38,281,733.9 | +| brace | *.{jpg,png,gif,webp} | [l=54] sunset...1920x1080.jpg | 169.206 ns | 5,909,952.3 | 169.354 ns | 5,904,782.4 | ✔ 36.770 ns | ✔ 27,196,127.6 | +| path | 0/1/2/3 | 0/1/2/3 | 23.348 ns | 42,830,676.1 | ✔ 17.064 ns | ✔ 58,604,216.9 | 17.255 ns | 57,953,639.5 | +| path | 0000/1111/2222/3333/4444 | 0000/1111/2222/3333/4444 | 43.299 ns | 23,095,357.4 | ✔ 32.740 ns | ✔ 30,543,364.7 | 30.771 ns | 32,498,539.7 | +| path, star | */*/*/*/*/*/*/* | 1/2/3/4/5/6/7/8 | 44.458 ns | 22,493,287.7 | ✔ 29.100 ns | ✔ 34,364,201.0 | 28.800 ns | 34,722,461.3 | +| path, star | */*/*/*/*/*/*/*/*/*/*/*/*/*/*/* | [l=199,s=16] /chrome/browser/...ModuleTest.java | 97.207 ns | 10,287,286.8 | ✔ 61.376 ns | ✔ 16,293,119.6 | 60.752 ns | 16,460,293.2 | +| path, literal | /chrome/...ModuleTest.java | [l=199,s=16] /chrome/browser/...ModuleTest.java | 249.692 ns | 4,004,931.1 | ✔ 214.788 ns | ✔ 4,655,746.6 | 181.186 ns | 5,519,200.7 | From 7e7ee2bb8350831a6323f5b13773842faa1df02b Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 30 Apr 2026 17:57:44 +0500 Subject: [PATCH 7/7] Clean up --- Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj | 5 +++++ Ramstack.Globbing/Ramstack.Globbing.csproj | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj b/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj index 3b1b23f..4ee52d6 100644 --- a/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj +++ b/Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj @@ -13,6 +13,11 @@ + + + + + diff --git a/Ramstack.Globbing/Ramstack.Globbing.csproj b/Ramstack.Globbing/Ramstack.Globbing.csproj index 9be81d9..65755cd 100644 --- a/Ramstack.Globbing/Ramstack.Globbing.csproj +++ b/Ramstack.Globbing/Ramstack.Globbing.csproj @@ -33,10 +33,6 @@ true - - - - all