11using BenchmarkDotNet . Attributes ;
2+ using BenchmarkDotNet . Columns ;
3+ using BenchmarkDotNet . Jobs ;
4+ using System . Collections . Generic ;
5+ using System . Numerics ;
26using System . Runtime . CompilerServices ;
7+ using System . Runtime . InteropServices ;
8+ using System . Runtime . Intrinsics ;
39
410namespace Base58Encoding . Benchmarks ;
511
12+ [ SimpleJob ( RuntimeMoniker . Net90 ) ]
13+ [ SimpleJob ( RuntimeMoniker . Net10_0 ) ]
14+ [ HideColumns ( "Job" , "Error" , "StdDev" , "Median" , "RatioSD" ) ]
615public class CountLeadingZerosBenchmark
716{
817 private static Lazy < byte [ ] [ ] > _lazyTestData = new ( ( ) =>
918 {
10- // Decode the provided base58 addresses to get byte arrays with leading zeros
11- var addresses = new [ ]
12- {
13- "1A1zP12r2r2r1wq1" ,
14- "1111111111111111111114oLvT2" ,
15- "1111113LrYkRba5STgvA5UoLqzLxwP6XhtN6f" ,
16- "1BoatSLRHtKNngkdXEeobR76b53LETtpyT" ,
17- "11bJr6xq2UhxDqkdqNKoGPYYEVBy6cd3M"
18- } ;
19- var data = new byte [ addresses . Length ] [ ] ;
20- for ( int i = 0 ; i < addresses . Length ; i ++ )
21- {
22- data [ i ] = Base58 . Bitcoin . Decode ( addresses [ i ] ) ;
23- }
24- return data ;
25- } ) ;
19+ var testCases = new List < byte [ ] > ( ) ;
20+
21+ // Bitcoin addresses with varying leading zeros
22+ testCases . Add ( Base58 . Bitcoin . Decode ( "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" ) ) ; // 0 leading zeros (standard P2PKH)
23+ testCases . Add ( Base58 . Bitcoin . Decode ( "1111111111111111111114oLvT2" ) ) ; // Many leading zeros
24+ testCases . Add ( Base58 . Bitcoin . Decode ( "1BoatSLRHtKNngkdXEeobR76b53LETtpyT" ) ) ; // 0 leading zeros (standard)
25+
26+ // Solana addresses (32 bytes) - realistic cases
27+ testCases . Add ( Base58 . Bitcoin . Decode ( "11111111111111111111111111111112" ) ) ; // 31 leading zeros (system program)
28+ testCases . Add ( Base58 . Bitcoin . Decode ( "1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM" ) ) ; // 1 leading zero
29+ testCases . Add ( Base58 . Bitcoin . Decode ( "4TMFNY21gmHLT3HpPZDiXY6kQkGPpJsJRfisJ9T7rXdV" ) ) ; // Typical Solana address (0 leading zeros)
30+ testCases . Add ( Base58 . Bitcoin . Decode ( "So11111111111111111111111111111111111111112" ) ) ; // Wrapped SOL (0 leading zeros)
31+
32+ // IPFS hashes (46 bytes) - typical CIDv0
33+ testCases . Add ( Base58 . Bitcoin . Decode ( "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" ) ) ; // No leading zeros (typical)
2634
27- public byte [ ] [ ] TestData ;
35+ // Sui transaction digests (32 bytes)
36+ testCases . Add ( Base58 . Bitcoin . Decode ( "11111118YbNzKyoNPP4TJLYwpB2B3NCvpNXJbRNcszU" ) ) ; // Many leading zeros
37+ testCases . Add ( Base58 . Bitcoin . Decode ( "3aDawfe6rm6QnFhJGppyyHVxXfEhBGYPMD8nzfLKNJqq" ) ) ;
2838
39+ // Typical blockchain transaction hashes
40+ testCases . Add ( Base58 . Bitcoin . Decode ( "4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi" ) ) ; // 0 leading zeros
41+ testCases . Add ( Base58 . Bitcoin . Decode ( "11BxRVsYgGSUZRSMaNcNjSzwMBYXqZQvBQr3BQznBFmpV" ) ) ; // 2 leading zeros
42+
43+ return testCases . ToArray ( ) ;
44+ } ) ;
45+
46+ private byte [ ] [ ] TestData = default ! ;
2947
3048 [ GlobalSetup ]
3149 public void Setup ( )
@@ -50,7 +68,7 @@ public int Scalar()
5068 int totalCount = 0 ;
5169 foreach ( var data in TestData )
5270 {
53- totalCount += Base58 . CountLeadingZerosScalar ( data ) ;
71+ totalCount += CountLeadingZerosScalarImpl ( data ) ;
5472 }
5573 return totalCount ;
5674 }
@@ -61,13 +79,15 @@ public int SimdOnly()
6179 int totalCount = 0 ;
6280 foreach ( var data in TestData )
6381 {
64- int count = Base58 . CountLeadingZerosSimd ( data , out int processed ) ;
82+ int count = CountLeadingZerosSimdImpl ( data , out int processed ) ;
6583 if ( count < processed )
66- return count ;
67-
68- var total = count + Base58 . CountLeadingZerosScalar ( data . AsSpan ( count ) ) ;
69-
70- totalCount += total ;
84+ {
85+ totalCount += count ;
86+ }
87+ else
88+ {
89+ totalCount += count + CountLeadingZerosScalarImpl ( data . AsSpan ( count ) ) ;
90+ }
7191 }
7292
7393 return totalCount ;
@@ -79,12 +99,11 @@ public int Combined()
7999 int totalCount = 0 ;
80100 foreach ( var data in TestData )
81101 {
82- totalCount += Base58 . CountLeadingZeros ( data ) ;
102+ totalCount += CountLeadingZerosCombinedImpl ( data ) ;
83103 }
84104 return totalCount ;
85105 }
86106
87- // Simple array-based approach
88107 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
89108 private static int CountLeadingZerosArray ( ReadOnlySpan < byte > data )
90109 {
@@ -97,4 +116,110 @@ private static int CountLeadingZerosArray(ReadOnlySpan<byte> data)
97116 }
98117 return count ;
99118 }
119+
120+ // Copy of Base58.CountLeadingZerosScalar implementation
121+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
122+ private static int CountLeadingZerosScalarImpl ( ReadOnlySpan < byte > data )
123+ {
124+ int count = 0 ;
125+ int length = data . Length ;
126+ ref byte searchSpace = ref MemoryMarshal . GetReference ( data ) ;
127+
128+ while ( length >= sizeof ( ulong ) )
129+ {
130+ ulong value = Unsafe . ReadUnaligned < ulong > ( ref Unsafe . Add ( ref searchSpace , count ) ) ;
131+ if ( value != 0 )
132+ {
133+ if ( BitConverter . IsLittleEndian )
134+ {
135+ count += BitOperations . TrailingZeroCount ( value ) / 8 ;
136+ }
137+ else
138+ {
139+ count += BitOperations . LeadingZeroCount ( value ) / 8 ;
140+ }
141+ return count ;
142+ }
143+ count += sizeof ( ulong ) ;
144+ length -= sizeof ( ulong ) ;
145+ }
146+
147+ while ( count < data . Length && data [ count ] == 0 )
148+ {
149+ count ++ ;
150+ }
151+
152+ return count ;
153+ }
154+
155+ // Copy of Base58.CountLeadingZerosSimd implementation
156+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
157+ private static int CountLeadingZerosSimdImpl ( ReadOnlySpan < byte > data , out int processed )
158+ {
159+ int count = 0 ;
160+ int length = data . Length ;
161+ ref byte searchSpace = ref MemoryMarshal . GetReference ( data ) ;
162+
163+ if ( Vector256 . IsHardwareAccelerated && length >= Vector256 < byte > . Count )
164+ {
165+ var zeroVector = Vector256 < byte > . Zero ;
166+
167+ while ( length >= Vector256 < byte > . Count )
168+ {
169+ var vector = Vector256 . LoadUnsafe ( ref searchSpace , ( nuint ) count ) ;
170+ var comparison = Vector256 . Equals ( vector , zeroVector ) ;
171+ uint mask = comparison . ExtractMostSignificantBits ( ) ;
172+
173+ if ( mask != uint . MaxValue )
174+ {
175+ processed = count + Vector256 < byte > . Count ;
176+ return count + BitOperations . TrailingZeroCount ( ~ mask ) ;
177+ }
178+
179+ count += Vector256 < byte > . Count ;
180+ length -= Vector256 < byte > . Count ;
181+ }
182+ }
183+ else if ( Vector128 . IsHardwareAccelerated && length >= Vector128 < byte > . Count )
184+ {
185+ var zeroVector = Vector128 < byte > . Zero ;
186+
187+ while ( length >= Vector128 < byte > . Count )
188+ {
189+ var vector = Vector128 . LoadUnsafe ( ref searchSpace , ( nuint ) count ) ;
190+ var comparison = Vector128 . Equals ( vector , zeroVector ) ;
191+ uint mask = comparison . ExtractMostSignificantBits ( ) ;
192+
193+ if ( mask != ushort . MaxValue )
194+ {
195+ processed = count + Vector128 < byte > . Count ;
196+ return count + BitOperations . TrailingZeroCount ( ~ mask ) ;
197+ }
198+
199+ count += Vector128 < byte > . Count ;
200+ length -= Vector128 < byte > . Count ;
201+ }
202+ }
203+
204+ processed = count ;
205+ return count ;
206+ }
207+
208+ // Copy of Base58.CountLeadingZeros implementation
209+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
210+ private static int CountLeadingZerosCombinedImpl ( ReadOnlySpan < byte > data )
211+ {
212+ // intentionally commenting out, since other methods won't have it also
213+ //if (data.Length == 0)
214+ // return 0;
215+
216+ if ( data . Length < 16 )
217+ return CountLeadingZerosScalarImpl ( data ) ;
218+
219+ int count = CountLeadingZerosSimdImpl ( data , out int processed ) ;
220+ if ( count < processed )
221+ return count ;
222+
223+ return count + CountLeadingZerosScalarImpl ( data . Slice ( count ) ) ;
224+ }
100225}
0 commit comments