Skip to content

Commit 029d8ea

Browse files
authored
Merge pull request #35 from rameel/perf/speedup
Performance improvements: pick the low-hanging fruit
2 parents 3970b19 + 7e7ee2b commit 029d8ea

11 files changed

Lines changed: 533 additions & 39 deletions

File tree

.gitignore

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#Ignore thumbnails created by windows
1+
# Ignore thumbnails created by windows
22
[Tt]humbs.db
33

4-
#Ignore files build by Visual Studio
4+
# Ignore files build by Visual Studio
55
*.obj
66
*.exe
77
*.pdb
@@ -33,5 +33,8 @@ obj/
3333
*.ReSharper.user
3434
[Tt]est[Rr]esult*
3535

36-
#Ignore Rider/Idea files
36+
# Ignore Rider/Idea files
3737
.idea/
38+
39+
# Ignore BenchmarkDotNet artifacts
40+
BenchmarkDotNet.Artifacts
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using Ramstack.Globbing.Internal;
2+
3+
namespace Ramstack.Globbing;
4+
5+
[TestFixture]
6+
public unsafe class MemoryHelperTests
7+
{
8+
[Test]
9+
public void IndexOf_Char()
10+
{
11+
Span<char> buffer = stackalloc char[64];
12+
13+
while (buffer.Length != 0)
14+
{
15+
Fill(buffer);
16+
17+
var s = (char*)Unsafe.AsPointer(ref buffer[0]);
18+
Assert.That(
19+
MemoryHelper.IndexOf(s, s + buffer.Length, '*'),
20+
Is.EqualTo(-1));
21+
22+
for (var i = 0; i < buffer.Length; i++)
23+
{
24+
Fill(buffer);
25+
buffer[i] = '*';
26+
27+
Assert.That(
28+
MemoryHelper.IndexOf(s, s + buffer.Length, '*'),
29+
Is.EqualTo(i));
30+
}
31+
32+
buffer = buffer[1..];
33+
}
34+
}
35+
36+
[Test]
37+
public void IndexOfAny_CharChar()
38+
{
39+
Span<char> buffer = stackalloc char[64];
40+
41+
while (buffer.Length != 0)
42+
{
43+
Fill(buffer);
44+
var s = (char*)Unsafe.AsPointer(ref buffer[0]);
45+
46+
Assert.That(
47+
MemoryHelper.IndexOfAny(s, s + buffer.Length, '*', '?'),
48+
Is.EqualTo(-1));
49+
50+
Assert.That(
51+
MemoryHelper.IndexOfAny(s, s + buffer.Length, '?', '*'),
52+
Is.EqualTo(-1));
53+
54+
foreach (var needle in "*?")
55+
{
56+
for (var i = 0; i < buffer.Length; i++)
57+
{
58+
Fill(buffer);
59+
buffer[i] = needle;
60+
61+
Assert.That(
62+
MemoryHelper.IndexOfAny(s, s + buffer.Length, '*', '?'),
63+
Is.EqualTo(i));
64+
65+
Assert.That(
66+
MemoryHelper.IndexOfAny(s, s + buffer.Length, '?', '*'),
67+
Is.EqualTo(i));
68+
}
69+
}
70+
71+
if (buffer.Length > 2)
72+
{
73+
foreach (var needle in new[] { "*?", "?*" })
74+
{
75+
for (var i = 0; i < buffer.Length - 1; i++)
76+
{
77+
Fill(buffer);
78+
buffer[i + 0] = needle[0];
79+
buffer[i + 1] = needle[1];
80+
81+
var r = MemoryHelper.IndexOfAny(s, s + buffer.Length, '*', '?');
82+
Assert.That(r, Is.EqualTo(i));
83+
84+
var p = MemoryHelper.IndexOfAny(s, s + buffer.Length, '?', '*');
85+
Assert.That(p, Is.EqualTo(i));
86+
}
87+
}
88+
}
89+
90+
buffer = buffer[1..];
91+
}
92+
}
93+
94+
private static void Fill(Span<char> s)
95+
{
96+
for (var i = 0; i < s.Length; i++)
97+
{
98+
while (true)
99+
{
100+
var v = Random.Shared.Next(32, 127);
101+
if (v is '*' or '?' or '{' or '}' or '[' or '\\')
102+
continue;
103+
104+
s[i] = (char)v;
105+
break;
106+
}
107+
}
108+
}
109+
}

Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33
<TargetFramework>net6.0</TargetFramework>
44
<ImplicitUsings>enable</ImplicitUsings>
55
<Nullable>enable</Nullable>
6+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
67
<LangVersion>preview</LangVersion>
78
<RootNamespace>Ramstack.Globbing</RootNamespace>
89
</PropertyGroup>
910

1011
<ItemGroup>
12+
<Using Include="System.Runtime.CompilerServices" />
1113
<Using Include="NUnit.Framework" />
1214
</ItemGroup>
1315

16+
<ItemGroup>
17+
<Compile Include="..\Ramstack.Globbing\Internal\MemoryHelper.cs" Link="Internal\MemoryHelper.cs" />
18+
<Compile Include="..\Ramstack.Globbing\Internal\PathHelper.cs" Link="Internal\PathHelper.cs" />
19+
</ItemGroup>
20+
1421
<ItemGroup>
1522
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
1623
<PackageReference Include="NUnit" Version="4.3.2" />

Ramstack.Globbing.slnx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
<Solution>
2-
<Folder Name="/Solution Items/">
2+
<Folder Name="/.global/">
33
<File Path=".editorconfig" />
44
<File Path=".gitignore" />
55
<File Path="LICENSE" />
66
<File Path="README.md" />
77
</Folder>
8+
<Folder Name="/benchmarks/">
9+
<Project Path="benchmarks/Ramstack.Globbing.Benchmarks/Ramstack.Globbing.Benchmarks.csproj" />
10+
</Folder>
811
<Project Path="Ramstack.Globbing.Tests/Ramstack.Globbing.Tests.csproj" />
912
<Project Path="Ramstack.Globbing/Ramstack.Globbing.csproj" />
1013
</Solution>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Numerics;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.Intrinsics;
4+
using System.Runtime.Intrinsics.X86;
5+
6+
namespace Ramstack.Globbing.Internal;
7+
8+
internal static unsafe class MemoryHelper
9+
{
10+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
11+
public static int IndexOf(char* s, char* e, char ch)
12+
{
13+
var i = 0;
14+
15+
if (Sse2.IsSupported && s + Vector128<short>.Count <= e)
16+
{
17+
for (;;)
18+
{
19+
var result = Sse2.CompareEqual(
20+
Vector128.Create((short)ch),
21+
LoadVector(s));
22+
23+
var mask = Sse2.MoveMask(result.AsByte());
24+
if (mask != 0)
25+
{
26+
var offset = BitOperations.TrailingZeroCount(mask) >>> 1;
27+
return i + offset;
28+
}
29+
30+
s += Vector128<short>.Count;
31+
i += Vector128<short>.Count;
32+
33+
if (s + Vector128<short>.Count <= e)
34+
continue;
35+
36+
if (s == e)
37+
return -1;
38+
39+
//
40+
// Tail handling via the same SIMD path (no scalar fallback)
41+
//
42+
var remaining = (int)((nint)e - (nint)s) >>> 1;
43+
i = i + remaining - Vector128<short>.Count;
44+
s = e - Vector128<short>.Count;
45+
}
46+
}
47+
48+
for (; s < e; s++, i++)
49+
if (*s == ch)
50+
return i;
51+
52+
return -1;
53+
}
54+
55+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
56+
public static int IndexOfAny(char* s, char* e, char ch1, char ch2)
57+
{
58+
var i = 0;
59+
60+
if (Sse2.IsSupported && s + Vector128<short>.Count <= e)
61+
{
62+
for (;;)
63+
{
64+
var source = LoadVector(s);
65+
var result = Sse2.Or(
66+
Sse2.CompareEqual(source, Vector128.Create((short)ch1)),
67+
Sse2.CompareEqual(source, Vector128.Create((short)ch2))
68+
).AsByte();
69+
70+
var mask = Sse2.MoveMask(result);
71+
if (mask != 0)
72+
{
73+
var offset = BitOperations.TrailingZeroCount(mask) >>> 1;
74+
return i + offset;
75+
}
76+
77+
s += Vector128<short>.Count;
78+
i += Vector128<short>.Count;
79+
80+
if (s + Vector128<short>.Count <= e)
81+
continue;
82+
83+
if (s == e)
84+
return -1;
85+
86+
//
87+
// Tail handling via the same SIMD path (no scalar fallback)
88+
//
89+
var remaining = (int)((nint)e - (nint)s) >>> 1;
90+
i = i + remaining - Vector128<short>.Count;
91+
s = e - Vector128<short>.Count;
92+
}
93+
}
94+
95+
for (; s < e; s++, i++)
96+
if (*s == ch1 || *s == ch2)
97+
return i;
98+
99+
return -1;
100+
}
101+
102+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
103+
private static Vector128<short> LoadVector(void* source) =>
104+
Unsafe.ReadUnaligned<Vector128<short>>(source);
105+
}

0 commit comments

Comments
 (0)