1+ using BenchmarkDotNet . Attributes ;
2+ using System . Buffers . Binary ;
3+
4+ namespace Base58Encoding . Benchmarks ;
5+
6+ /// <summary>
7+ /// Benchmark comparing jagged arrays (uint[][]) vs multidimensional arrays (uint[,])
8+ /// for Base58 table lookup performance using the same Fast32 encode/decode logic
9+ /// Verdict - on encoding both are the same, but on decoding jagged arrays are 5-10% faster.
10+ /// </summary>
11+ [ MemoryDiagnoser ]
12+ public class JaggedVsMultidimensionalArrayBenchmark
13+ {
14+ private byte [ ] _data = default ! ;
15+ private string _encodedBase58 = default ! ;
16+
17+ // Jagged arrays (current implementation)
18+ private static readonly uint [ ] [ ] JaggedEncodeTable32 = Base58BitcoinTables . EncodeTable32 ;
19+ private static readonly uint [ ] [ ] JaggedDecodeTable32 = Base58BitcoinTables . DecodeTable32 ;
20+
21+ // Multidimensional arrays (alternative implementation)
22+ private static readonly uint [ , ] MultidimensionalEncodeTable32 = ConvertToMultidimensional ( Base58BitcoinTables . EncodeTable32 ) ;
23+ private static readonly uint [ , ] MultidimensionalDecodeTable32 = ConvertToMultidimensional ( Base58BitcoinTables . DecodeTable32 ) ;
24+
25+ [ GlobalSetup ]
26+ public void Setup ( )
27+ {
28+ _data = new byte [ 32 ] ;
29+ Random . Shared . NextBytes ( _data ) ;
30+ _encodedBase58 = SimpleBase . Base58 . Bitcoin . Encode ( _data ) ;
31+ }
32+
33+ [ Benchmark ( Baseline = true ) ]
34+ public string EncodeWithJaggedArray ( )
35+ {
36+ return EncodeBitcoin32FastJagged ( _data ) ;
37+ }
38+
39+ [ Benchmark ]
40+ public string EncodeWithMultidimensionalArray ( )
41+ {
42+ return EncodeBitcoin32FastMultidimensional ( _data ) ;
43+ }
44+
45+ [ Benchmark ]
46+ public byte [ ] DecodeWithJaggedArray ( )
47+ {
48+ return DecodeBitcoin32FastJagged ( _encodedBase58 ) ! ;
49+ }
50+
51+ [ Benchmark ]
52+ public byte [ ] DecodeWithMultidimensionalArray ( )
53+ {
54+ return DecodeBitcoin32FastMultidimensional ( _encodedBase58 ) ! ;
55+ }
56+
57+ private static uint [ , ] ConvertToMultidimensional ( uint [ ] [ ] jaggedArray )
58+ {
59+ int rows = jaggedArray . Length ;
60+ int cols = jaggedArray [ 0 ] . Length ;
61+ var result = new uint [ rows , cols ] ;
62+
63+ for ( int i = 0 ; i < rows ; i ++ )
64+ {
65+ for ( int j = 0 ; j < cols ; j ++ )
66+ {
67+ result [ i , j ] = jaggedArray [ i ] [ j ] ;
68+ }
69+ }
70+
71+ return result ;
72+ }
73+
74+ private static string EncodeBitcoin32FastJagged ( ReadOnlySpan < byte > data )
75+ {
76+ // Count leading zeros
77+ int inLeadingZeros = Base58 . CountLeadingZeros ( data ) ;
78+
79+ if ( inLeadingZeros == data . Length )
80+ {
81+ return new string ( '1' , inLeadingZeros ) ;
82+ }
83+
84+ // Convert 32 bytes to 8 uint32 limbs (big-endian)
85+ Span < uint > binary = stackalloc uint [ Base58BitcoinTables . BinarySz32 ] ;
86+ for ( int i = 0 ; i < Base58BitcoinTables . BinarySz32 ; i ++ )
87+ {
88+ int offset = i * sizeof ( uint ) ;
89+ binary [ i ] = BinaryPrimitives . ReadUInt32BigEndian ( data . Slice ( offset , sizeof ( uint ) ) ) ;
90+ }
91+
92+ // Convert to intermediate format (base 58^5) using JAGGED ARRAY
93+ Span < ulong > intermediate = stackalloc ulong [ Base58BitcoinTables . IntermediateSz32 ] ;
94+ intermediate . Clear ( ) ;
95+
96+ for ( int i = 0 ; i < Base58BitcoinTables . BinarySz32 ; i ++ )
97+ {
98+ for ( int j = 0 ; j < Base58BitcoinTables . IntermediateSz32 - 1 ; j ++ )
99+ {
100+ intermediate [ j + 1 ] += ( ulong ) binary [ i ] * JaggedEncodeTable32 [ i ] [ j ] ;
101+ }
102+ }
103+
104+ // Reduce each term to be less than 58^5
105+ for ( int i = Base58BitcoinTables . IntermediateSz32 - 1 ; i > 0 ; i -- )
106+ {
107+ intermediate [ i - 1 ] += intermediate [ i ] / Base58BitcoinTables . R1Div ;
108+ intermediate [ i ] %= Base58BitcoinTables . R1Div ;
109+ }
110+
111+ // Convert intermediate form to raw base58 digits
112+ Span < byte > rawBase58 = stackalloc byte [ Base58BitcoinTables . Raw58Sz32 ] ;
113+ for ( int i = 0 ; i < Base58BitcoinTables . IntermediateSz32 ; i ++ )
114+ {
115+ uint v = ( uint ) intermediate [ i ] ;
116+ rawBase58 [ 5 * i + 4 ] = ( byte ) ( ( v / 1U ) % 58U ) ;
117+ rawBase58 [ 5 * i + 3 ] = ( byte ) ( ( v / 58U ) % 58U ) ;
118+ rawBase58 [ 5 * i + 2 ] = ( byte ) ( ( v / 3364U ) % 58U ) ;
119+ rawBase58 [ 5 * i + 1 ] = ( byte ) ( ( v / 195112U ) % 58U ) ;
120+ rawBase58 [ 5 * i + 0 ] = ( byte ) ( v / 11316496U ) ;
121+ }
122+
123+ // Count leading zeros in raw output
124+ int rawLeadingZeros = 0 ;
125+ for ( ; rawLeadingZeros < Base58BitcoinTables . Raw58Sz32 ; rawLeadingZeros ++ )
126+ {
127+ if ( rawBase58 [ rawLeadingZeros ] != 0 ) break ;
128+ }
129+
130+ // Calculate skip and final length
131+ int skip = rawLeadingZeros - inLeadingZeros ;
132+ int outputLength = Base58BitcoinTables . Raw58Sz32 - skip ;
133+ var state = new Base58 . EncodeFastState ( rawBase58 , inLeadingZeros , rawLeadingZeros , outputLength ) ;
134+ return string . Create ( outputLength , state , static ( span , state ) =>
135+ {
136+ if ( state . InLeadingZeros > 0 )
137+ {
138+ span [ ..state . InLeadingZeros ] . Fill ( '1' ) ;
139+ }
140+
141+ var bitcoinChars = Base58BitcoinTables . BitcoinChars ;
142+ for ( int i = 0 ; i < state . OutputLength - state . InLeadingZeros ; i ++ )
143+ {
144+ byte digit = state . RawBase58 [ state . RawLeadingZeros + i ] ;
145+ span [ state . InLeadingZeros + i ] = bitcoinChars [ digit ] ;
146+ }
147+ } ) ;
148+ }
149+
150+ private static string EncodeBitcoin32FastMultidimensional ( ReadOnlySpan < byte > data )
151+ {
152+ // Count leading zeros
153+ int inLeadingZeros = Base58 . CountLeadingZeros ( data ) ;
154+
155+ if ( inLeadingZeros == data . Length )
156+ {
157+ return new string ( '1' , inLeadingZeros ) ;
158+ }
159+
160+ // Convert 32 bytes to 8 uint32 limbs (big-endian)
161+ Span < uint > binary = stackalloc uint [ Base58BitcoinTables . BinarySz32 ] ;
162+ for ( int i = 0 ; i < Base58BitcoinTables . BinarySz32 ; i ++ )
163+ {
164+ int offset = i * sizeof ( uint ) ;
165+ binary [ i ] = BinaryPrimitives . ReadUInt32BigEndian ( data . Slice ( offset , sizeof ( uint ) ) ) ;
166+ }
167+
168+ // Convert to intermediate format (base 58^5) using MULTIDIMENSIONAL ARRAY
169+ Span < ulong > intermediate = stackalloc ulong [ Base58BitcoinTables . IntermediateSz32 ] ;
170+ intermediate . Clear ( ) ;
171+
172+ for ( int i = 0 ; i < Base58BitcoinTables . BinarySz32 ; i ++ )
173+ {
174+ for ( int j = 0 ; j < Base58BitcoinTables . IntermediateSz32 - 1 ; j ++ )
175+ {
176+ intermediate [ j + 1 ] += ( ulong ) binary [ i ] * MultidimensionalEncodeTable32 [ i , j ] ;
177+ }
178+ }
179+
180+ // Reduce each term to be less than 58^5
181+ for ( int i = Base58BitcoinTables . IntermediateSz32 - 1 ; i > 0 ; i -- )
182+ {
183+ intermediate [ i - 1 ] += intermediate [ i ] / Base58BitcoinTables . R1Div ;
184+ intermediate [ i ] %= Base58BitcoinTables . R1Div ;
185+ }
186+
187+ // Convert intermediate form to raw base58 digits
188+ Span < byte > rawBase58 = stackalloc byte [ Base58BitcoinTables . Raw58Sz32 ] ;
189+ for ( int i = 0 ; i < Base58BitcoinTables . IntermediateSz32 ; i ++ )
190+ {
191+ uint v = ( uint ) intermediate [ i ] ;
192+ rawBase58 [ 5 * i + 4 ] = ( byte ) ( ( v / 1U ) % 58U ) ;
193+ rawBase58 [ 5 * i + 3 ] = ( byte ) ( ( v / 58U ) % 58U ) ;
194+ rawBase58 [ 5 * i + 2 ] = ( byte ) ( ( v / 3364U ) % 58U ) ;
195+ rawBase58 [ 5 * i + 1 ] = ( byte ) ( ( v / 195112U ) % 58U ) ;
196+ rawBase58 [ 5 * i + 0 ] = ( byte ) ( v / 11316496U ) ;
197+ }
198+
199+ // Count leading zeros in raw output
200+ int rawLeadingZeros = 0 ;
201+ for ( ; rawLeadingZeros < Base58BitcoinTables . Raw58Sz32 ; rawLeadingZeros ++ )
202+ {
203+ if ( rawBase58 [ rawLeadingZeros ] != 0 ) break ;
204+ }
205+
206+ // Calculate skip and final length
207+ int skip = rawLeadingZeros - inLeadingZeros ;
208+ int outputLength = Base58BitcoinTables . Raw58Sz32 - skip ;
209+
210+ var state = new Base58 . EncodeFastState ( rawBase58 , inLeadingZeros , rawLeadingZeros , outputLength ) ;
211+ return string . Create ( outputLength , state , static ( span , state ) =>
212+ {
213+ if ( state . InLeadingZeros > 0 )
214+ {
215+ span [ ..state . InLeadingZeros ] . Fill ( '1' ) ;
216+ }
217+
218+ var bitcoinChars = Base58BitcoinTables . BitcoinChars ;
219+ for ( int i = 0 ; i < state . OutputLength - state . InLeadingZeros ; i ++ )
220+ {
221+ byte digit = state . RawBase58 [ state . RawLeadingZeros + i ] ;
222+ span [ state . InLeadingZeros + i ] = bitcoinChars [ digit ] ;
223+ }
224+ } ) ;
225+ }
226+
227+ private static byte [ ] ? DecodeBitcoin32FastJagged ( string encoded )
228+ {
229+ // Early validation and length check
230+ if ( encoded . Length > Base58BitcoinTables . Raw58Sz32 ) return null ;
231+
232+ // Validate characters and create raw array using JAGGED ARRAY lookup
233+ Span < byte > rawBase58 = stackalloc byte [ Base58BitcoinTables . Raw58Sz32 ] ;
234+ var bitcoinDecodeTable = Base58Alphabet . Bitcoin . DecodeTable . Span ;
235+
236+ int prepend0 = Base58BitcoinTables . Raw58Sz32 - encoded . Length ;
237+ for ( int j = 0 ; j < Base58BitcoinTables . Raw58Sz32 ; j ++ )
238+ {
239+ if ( j < prepend0 )
240+ {
241+ rawBase58 [ j ] = 0 ;
242+ }
243+ else
244+ {
245+ char c = encoded [ j - prepend0 ] ;
246+ if ( c >= 128 || bitcoinDecodeTable [ c ] == 255 )
247+ return null ;
248+
249+ rawBase58 [ j ] = bitcoinDecodeTable [ c ] ;
250+ }
251+ }
252+
253+ // Convert to intermediate format
254+ Span < ulong > intermediate = stackalloc ulong [ Base58BitcoinTables . IntermediateSz32 ] ;
255+ for ( int i = 0 ; i < Base58BitcoinTables . IntermediateSz32 ; i ++ )
256+ {
257+ intermediate [ i ] = ( ulong ) rawBase58 [ 5 * i + 0 ] * 11316496UL +
258+ ( ulong ) rawBase58 [ 5 * i + 1 ] * 195112UL +
259+ ( ulong ) rawBase58 [ 5 * i + 2 ] * 3364UL +
260+ ( ulong ) rawBase58 [ 5 * i + 3 ] * 58UL +
261+ ( ulong ) rawBase58 [ 5 * i + 4 ] * 1UL ;
262+ }
263+
264+ // Convert to binary using JAGGED ARRAY
265+ Span < ulong > binary = stackalloc ulong [ Base58BitcoinTables . BinarySz32 ] ;
266+ for ( int j = 0 ; j < Base58BitcoinTables . BinarySz32 ; j ++ )
267+ {
268+ ulong acc = 0UL ;
269+ for ( int i = 0 ; i < Base58BitcoinTables . IntermediateSz32 ; i ++ )
270+ {
271+ acc += intermediate [ i ] * JaggedDecodeTable32 [ i ] [ j ] ;
272+ }
273+ binary [ j ] = acc ;
274+ }
275+
276+ // Reduce to proper uint32 values
277+ for ( int i = Base58BitcoinTables . BinarySz32 - 1 ; i > 0 ; i -- )
278+ {
279+ binary [ i - 1 ] += binary [ i ] >> 32 ;
280+ binary [ i ] &= 0xFFFFFFFFUL ;
281+ }
282+
283+ if ( binary [ 0 ] > 0xFFFFFFFFUL ) return null ;
284+
285+ // Convert to output bytes
286+ var result = new byte [ 32 ] ;
287+ for ( int i = 0 ; i < Base58BitcoinTables . BinarySz32 ; i ++ )
288+ {
289+ BinaryPrimitives . WriteUInt32BigEndian ( result . AsSpan ( i * 4 , 4 ) , ( uint ) binary [ i ] ) ;
290+ }
291+
292+ // Validate leading zeros match leading '1's
293+ int leadingZeroCnt = 0 ;
294+ for ( ; leadingZeroCnt < 32 ; leadingZeroCnt ++ )
295+ {
296+ if ( result [ leadingZeroCnt ] != 0 ) break ;
297+ if ( encoded . Length <= leadingZeroCnt || encoded [ leadingZeroCnt ] != '1' ) return null ;
298+ }
299+ if ( leadingZeroCnt < encoded . Length && encoded [ leadingZeroCnt ] == '1' ) return null ;
300+
301+ return result ;
302+ }
303+
304+ private static byte [ ] ? DecodeBitcoin32FastMultidimensional ( string encoded )
305+ {
306+ // Early validation and length check
307+ if ( encoded . Length > Base58BitcoinTables . Raw58Sz32 ) return null ;
308+
309+ // Validate characters and create raw array using MULTIDIMENSIONAL ARRAY lookup
310+ Span < byte > rawBase58 = stackalloc byte [ Base58BitcoinTables . Raw58Sz32 ] ;
311+ var bitcoinDecodeTable = Base58Alphabet . Bitcoin . DecodeTable . Span ;
312+
313+ int prepend0 = Base58BitcoinTables . Raw58Sz32 - encoded . Length ;
314+ for ( int j = 0 ; j < Base58BitcoinTables . Raw58Sz32 ; j ++ )
315+ {
316+ if ( j < prepend0 )
317+ {
318+ rawBase58 [ j ] = 0 ;
319+ }
320+ else
321+ {
322+ char c = encoded [ j - prepend0 ] ;
323+ if ( c >= 128 || bitcoinDecodeTable [ c ] == 255 )
324+ return null ;
325+
326+ rawBase58 [ j ] = bitcoinDecodeTable [ c ] ;
327+ }
328+ }
329+
330+ // Convert to intermediate format
331+ Span < ulong > intermediate = stackalloc ulong [ Base58BitcoinTables . IntermediateSz32 ] ;
332+ for ( int i = 0 ; i < Base58BitcoinTables . IntermediateSz32 ; i ++ )
333+ {
334+ intermediate [ i ] = ( ulong ) rawBase58 [ 5 * i + 0 ] * 11316496UL +
335+ ( ulong ) rawBase58 [ 5 * i + 1 ] * 195112UL +
336+ ( ulong ) rawBase58 [ 5 * i + 2 ] * 3364UL +
337+ ( ulong ) rawBase58 [ 5 * i + 3 ] * 58UL +
338+ ( ulong ) rawBase58 [ 5 * i + 4 ] * 1UL ;
339+ }
340+
341+ // Convert to binary using MULTIDIMENSIONAL ARRAY
342+ Span < ulong > binary = stackalloc ulong [ Base58BitcoinTables . BinarySz32 ] ;
343+ for ( int j = 0 ; j < Base58BitcoinTables . BinarySz32 ; j ++ )
344+ {
345+ ulong acc = 0UL ;
346+ for ( int i = 0 ; i < Base58BitcoinTables . IntermediateSz32 ; i ++ )
347+ {
348+ acc += intermediate [ i ] * MultidimensionalDecodeTable32 [ i , j ] ;
349+ }
350+ binary [ j ] = acc ;
351+ }
352+
353+ // Reduce to proper uint32 values
354+ for ( int i = Base58BitcoinTables . BinarySz32 - 1 ; i > 0 ; i -- )
355+ {
356+ binary [ i - 1 ] += binary [ i ] >> 32 ;
357+ binary [ i ] &= 0xFFFFFFFFUL ;
358+ }
359+
360+ if ( binary [ 0 ] > 0xFFFFFFFFUL ) return null ;
361+
362+ // Convert to output bytes
363+ var result = new byte [ 32 ] ;
364+ for ( int i = 0 ; i < Base58BitcoinTables . BinarySz32 ; i ++ )
365+ {
366+ BinaryPrimitives . WriteUInt32BigEndian ( result . AsSpan ( i * 4 , 4 ) , ( uint ) binary [ i ] ) ;
367+ }
368+
369+ // Validate leading zeros match leading '1's
370+ int leadingZeroCnt = 0 ;
371+ for ( ; leadingZeroCnt < 32 ; leadingZeroCnt ++ )
372+ {
373+ if ( result [ leadingZeroCnt ] != 0 ) break ;
374+ if ( encoded . Length <= leadingZeroCnt || encoded [ leadingZeroCnt ] != '1' ) return null ;
375+ }
376+ if ( leadingZeroCnt < encoded . Length && encoded [ leadingZeroCnt ] == '1' ) return null ;
377+
378+ return result ;
379+ }
380+ }
0 commit comments