Skip to content

Commit 84f6201

Browse files
committed
feat: simplify counting leading characters
1 parent 6fc49b3 commit 84f6201

File tree

3 files changed

+134
-29
lines changed

3 files changed

+134
-29
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Columns;
3+
using BenchmarkDotNet.Jobs;
4+
using System.Collections.Generic;
5+
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
7+
using static System.Net.Mime.MediaTypeNames;
8+
9+
namespace Base58Encoding.Benchmarks;
10+
11+
[SimpleJob(RuntimeMoniker.Net90)]
12+
[SimpleJob(RuntimeMoniker.Net10_0)]
13+
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
14+
[MemoryDiagnoser]
15+
public class CountLeadingCharactersBenchmark
16+
{
17+
private static readonly string[] TestStrings = new[]
18+
{
19+
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", // 0 leading zeros (standard P2PKH)
20+
"111111111111111111114oLvT2", // Many leading zeros
21+
"1BoatSLRHtKNngkdXEeobR76b53LETtpyT", // 0 leading zeros (standard)
22+
23+
"11111111111111111111111111111112", // 31 leading zeros (system program)
24+
"1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM", // 1 leading zero
25+
"4TMFNY21gmHLT3HpPZDiXY6kQkGPpJsJRfisJ9T7rXdV", // Typical Solana address (0 leading zeros)
26+
"So11111111111111111111111111111111111111112", // Wrapped SOL (0 leading zeros)
27+
28+
"QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", // No leading zeros (typical)
29+
"11111118YbNzKyoNPP4TJLYwpB2B3NCvpNXJbRNcszU", // Many leading zeros
30+
"3aDawfe6rm6QnFhJGppyyHVxXfEhBGYPMD8nzfLKNJqq",
31+
"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi", // 0 leading zeros
32+
"11BxRVsYgGSUZRSMaNcNjSzwMBYXqZQvBQr3BQznBFmpV" // 2 leading zeros
33+
};
34+
35+
[Benchmark(Description = "Simple Loop")]
36+
public int SimpleLoop()
37+
{
38+
int totalCount = 0;
39+
foreach (var str in TestStrings)
40+
{
41+
totalCount += CountLeadingCharactersSimple(str, '1');
42+
}
43+
return totalCount;
44+
}
45+
46+
[Benchmark(Baseline = true, Description = "Optimized sequential")]
47+
public int Optimized()
48+
{
49+
int totalCount = 0;
50+
foreach (var str in TestStrings)
51+
{
52+
totalCount += CountLeadingCharacters(str, '1');
53+
}
54+
return totalCount;
55+
}
56+
57+
[Benchmark(Description = "IndexOfAnyExcept")]
58+
public int IndexOfAnyExcept()
59+
{
60+
int totalCount = 0;
61+
foreach (var str in TestStrings)
62+
{
63+
totalCount += CountLeadingCharactersExcept(str, '1');
64+
}
65+
66+
return totalCount;
67+
}
68+
69+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
70+
public int CountLeadingCharactersExcept(ReadOnlySpan<char> text, char target)
71+
{
72+
int mismatchIndex = text.IndexOfAnyExcept(target);
73+
74+
return mismatchIndex == -1 ? text.Length : mismatchIndex;
75+
}
76+
77+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
78+
private static int CountLeadingCharactersSimple(ReadOnlySpan<char> text, char target)
79+
{
80+
int count = 0;
81+
for (int i = 0; i < text.Length; i++)
82+
{
83+
if (text[i] != target)
84+
break;
85+
count++;
86+
}
87+
return count;
88+
}
89+
90+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
91+
internal static int CountLeadingCharacters(ReadOnlySpan<char> text, char target)
92+
{
93+
int count = 0;
94+
int length = text.Length;
95+
ref char searchSpace = ref MemoryMarshal.GetReference(text);
96+
ulong targetPattern = ((ulong)target) | (((ulong)target) << 16) | (((ulong)target) << 32) | (((ulong)target) << 48);
97+
98+
while (length >= 4)
99+
{
100+
ulong fourChars = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref searchSpace, count)));
101+
102+
if (fourChars != targetPattern)
103+
{
104+
if (BitConverter.IsLittleEndian)
105+
{
106+
if ((fourChars & 0xFFFF) != target) return count;
107+
if (((fourChars >> 16) & 0xFFFF) != target) return count + 1;
108+
if (((fourChars >> 32) & 0xFFFF) != target) return count + 2;
109+
return count + 3;
110+
}
111+
else
112+
{
113+
if (((fourChars >> 48) & 0xFFFF) != target) return count;
114+
if (((fourChars >> 32) & 0xFFFF) != target) return count + 1;
115+
if (((fourChars >> 16) & 0xFFFF) != target) return count + 2;
116+
return count + 3;
117+
}
118+
}
119+
120+
count += 4;
121+
length -= 4;
122+
}
123+
124+
while (count < text.Length && Unsafe.Add(ref searchSpace, count) == target)
125+
{
126+
count++;
127+
}
128+
129+
return count;
130+
}
131+
}

src/Base58Encoding.Benchmarks/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
using Base58Encoding.Benchmarks;
33

44
//BenchmarkRunner.Run<Base58ComparisonBenchmark>();
5-
BenchmarkRunner.Run<CountLeadingZerosBenchmark>();
5+
BenchmarkRunner.Run<CountLeadingCharactersBenchmark>();
66
//BenchmarkRunner.Run<BoundsCheckComparisonBenchmark>();

src/Base58Encoding/CountLeading.Base58.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,34 +109,8 @@ internal static int CountLeadingZerosScalar(ReadOnlySpan<byte> data)
109109
[MethodImpl(MethodImplOptions.AggressiveInlining)]
110110
internal static int CountLeadingCharacters(ReadOnlySpan<char> text, char target)
111111
{
112-
int count = 0;
113-
ref char searchSpace = ref MemoryMarshal.GetReference(text);
114-
115-
int length = text.Length;
116-
117-
while (length >= 4 && count + 3 < text.Length)
118-
{
119-
ulong fourChars = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref searchSpace, count)));
120-
121-
ulong targetPattern = ((ulong)target) | (((ulong)target) << 16) | (((ulong)target) << 32) | (((ulong)target) << 48);
122-
123-
if (fourChars != targetPattern)
124-
{
125-
if (Unsafe.Add(ref searchSpace, count) != target) return count;
126-
if (Unsafe.Add(ref searchSpace, count + 1) != target) return count + 1;
127-
if (Unsafe.Add(ref searchSpace, count + 2) != target) return count + 2;
128-
return count + 3;
129-
}
112+
int mismatchIndex = text.IndexOfAnyExcept(target);
130113

131-
count += 4;
132-
length -= 4;
133-
}
134-
135-
while (count < text.Length && Unsafe.Add(ref searchSpace, count) == target)
136-
{
137-
count++;
138-
}
139-
140-
return count;
114+
return mismatchIndex == -1 ? text.Length : mismatchIndex;
141115
}
142116
}

0 commit comments

Comments
 (0)