Skip to content

Commit b1e0abb

Browse files
committed
bench: benchmark jagged vs multidimensional access
1 parent 154c987 commit b1e0abb

3 files changed

Lines changed: 384 additions & 7 deletions

File tree

src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
using BenchmarkDotNet.Attributes;
2-
using BenchmarkDotNet.Columns;
32
using BenchmarkDotNet.Diagnosers;
4-
using BenchmarkDotNet.Jobs;
53
using Base58Encoding.Benchmarks.Common;
64

75
namespace Base58Encoding.Benchmarks;
86

97
[MemoryDiagnoser]
10-
//[DisassemblyDiagnoser(exportCombinedDisassemblyReport: true)]
118
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
129
public class Base58ComparisonBenchmark
1310
{
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
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

Comments
 (0)