Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 36 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ These optimizations are based on Firedancer's specialized Base58 algorithms and
- Precomputed multiplication tables replace expensive division operations
- Converts binary data to base 58^5 limbs, then to raw base58 digits
- Matrix multiplication approach processes 5 base58 digits simultaneously
- The 32/64-byte encode and decode matrix kernels are SIMD-accelerated with `Vector256`/`Vector128` (widest available width, scalar fallback) — in addition to the vectorized leading-zero count
- Separate encode/decode tables for 32-byte and 64-byte fixed sizes
- Achieves ~2.5x speedup through table-based optimizations vs iterative division

Expand All @@ -81,43 +82,43 @@ These optimizations are based on Firedancer's specialized Base58 algorithms and

```

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8246/25H2/2025Update/HudsonValley2)
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2)
13th Gen Intel Core i7-13700KF 3.40GHz, 1 CPU, 24 logical and 16 physical cores
.NET SDK 10.0.203
[Host] : .NET 10.0.7 (10.0.7, 10.0.726.21808), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.7 (10.0.7, 10.0.726.21808), X64 RyuJIT x86-64-v3
.NET SDK 11.0.100-preview.3.26207.106
[Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

Job=DefaultJob

```
| Method | VectorType | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|--------------------------- |--------------- |------------:|------:|-------:|----------:|------------:|
| **'Our Base58 Encode'** | **BitcoinAddress** | **537.17 ns** | **1.00** | **0.0057** | **96 B** | **1.00** |
| 'SimpleBase Base58 Encode' | BitcoinAddress | 776.69 ns | 1.45 | 0.0057 | 96 B | 1.00 |
| 'Our Base58 Decode' | BitcoinAddress | 160.88 ns | 0.30 | 0.0033 | 56 B | 0.58 |
| 'SimpleBase Base58 Decode' | BitcoinAddress | 353.19 ns | 0.66 | 0.0033 | 56 B | 0.58 |
| | | | | | | |
| **'Our Base58 Encode'** | **SolanaAddress** | **94.07 ns** | **1.00** | **0.0070** | **112 B** | **1.00** |
| 'SimpleBase Base58 Encode' | SolanaAddress | 1,433.92 ns | 15.24 | 0.0057 | 112 B | 1.00 |
| 'Our Base58 Decode' | SolanaAddress | 104.19 ns | 1.11 | 0.0035 | 56 B | 0.50 |
| 'SimpleBase Base58 Decode' | SolanaAddress | 703.66 ns | 7.48 | 0.0029 | 56 B | 0.50 |
| | | | | | | |
| **'Our Base58 Encode'** | **SolanaTx** | **239.21 ns** | **1.00** | **0.0124** | **200 B** | **1.00** |
| 'SimpleBase Base58 Encode' | SolanaTx | 7,166.10 ns | 29.96 | 0.0076 | 200 B | 1.00 |
| 'Our Base58 Decode' | SolanaTx | 180.37 ns | 0.75 | 0.0055 | 88 B | 0.44 |
| 'SimpleBase Base58 Decode' | SolanaTx | 2,957.77 ns | 12.36 | 0.0038 | 88 B | 0.44 |
| | | | | | | |
| **'Our Base58 Encode'** | **IPFSHash** | **1,084.69 ns** | **1.00** | **0.0076** | **120 B** | **1.00** |
| 'SimpleBase Base58 Encode' | IPFSHash | 1,617.11 ns | 1.49 | 0.0076 | 120 B | 1.00 |
| 'Our Base58 Decode' | IPFSHash | 318.15 ns | 0.29 | 0.0038 | 64 B | 0.53 |
| 'SimpleBase Base58 Decode' | IPFSHash | 854.47 ns | 0.79 | 0.0038 | 64 B | 0.53 |
| | | | | | | |
| **'Our Base58 Encode'** | **MoneroAddress** | **4,917.65 ns** | **1.00** | **0.0076** | **216 B** | **1.00** |
| 'SimpleBase Base58 Encode' | MoneroAddress | 8,621.98 ns | 1.75 | - | 216 B | 1.00 |
| 'Our Base58 Decode' | MoneroAddress | 1,198.92 ns | 0.24 | 0.0057 | 96 B | 0.44 |
| 'SimpleBase Base58 Decode' | MoneroAddress | 3,844.43 ns | 0.78 | - | 96 B | 0.44 |


## License

This project is available under the MIT License.
| Method | Categories | VectorType | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
|--------------------------- |----------- |--------------- |------------:|------:|-------:|----------:|------------:|
| **'Our Base58 Decode'** | **Decode** | **BitcoinAddress** | **178.71 ns** | **1.00** | **0.0033** | **56 B** | **1.00** |
| 'SimpleBase Base58 Decode' | Decode | BitcoinAddress | 364.51 ns | 2.04 | 0.0033 | 56 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Decode'** | **Decode** | **SolanaAddress** | **83.27 ns** | **1.00** | **0.0035** | **56 B** | **1.00** |
| 'SimpleBase Base58 Decode' | Decode | SolanaAddress | 598.64 ns | 7.19 | 0.0029 | 56 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Decode'** | **Decode** | **SolanaTx** | **167.00 ns** | **1.00** | **0.0055** | **88 B** | **1.00** |
| 'SimpleBase Base58 Decode' | Decode | SolanaTx | 4,190.80 ns | 25.10 | 0.0038 | 88 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Decode'** | **Decode** | **IPFSHash** | **339.26 ns** | **1.00** | **0.0038** | **64 B** | **1.00** |
| 'SimpleBase Base58 Decode' | Decode | IPFSHash | 641.17 ns | 1.89 | 0.0038 | 64 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Decode'** | **Decode** | **MoneroAddress** | **1,391.11 ns** | **1.00** | **0.0057** | **96 B** | **1.00** |
| 'SimpleBase Base58 Decode' | Decode | MoneroAddress | 3,882.84 ns | 2.79 | - | 96 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Encode'** | **Encode** | **BitcoinAddress** | **532.69 ns** | **1.00** | **0.0057** | **96 B** | **1.00** |
| 'SimpleBase Base58 Encode' | Encode | BitcoinAddress | 774.83 ns | 1.45 | 0.0057 | 96 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Encode'** | **Encode** | **SolanaAddress** | **95.15 ns** | **1.00** | **0.0070** | **112 B** | **1.00** |
| 'SimpleBase Base58 Encode' | Encode | SolanaAddress | 1,521.61 ns | 15.99 | 0.0057 | 112 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Encode'** | **Encode** | **SolanaTx** | **196.50 ns** | **1.00** | **0.0126** | **200 B** | **1.00** |
| 'SimpleBase Base58 Encode' | Encode | SolanaTx | 7,338.14 ns | 37.34 | 0.0076 | 200 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Encode'** | **Encode** | **IPFSHash** | **1,085.19 ns** | **1.00** | **0.0076** | **120 B** | **1.00** |
| 'SimpleBase Base58 Encode' | Encode | IPFSHash | 1,690.70 ns | 1.56 | 0.0076 | 120 B | 1.00 |
| | | | | | | | |
| **'Our Base58 Encode'** | **Encode** | **MoneroAddress** | **4,959.86 ns** | **1.00** | **0.0076** | **216 B** | **1.00** |
| 'SimpleBase Base58 Encode' | Encode | MoneroAddress | 8,734.32 ns | 1.76 | - | 216 B | 1.00 |
11 changes: 7 additions & 4 deletions src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using Base58Encoding.Benchmarks.Common;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;

namespace Base58Encoding.Benchmarks;

[MemoryDiagnoser]
[CategoriesColumn]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public class Base58ComparisonBenchmark
{
Expand All @@ -28,25 +31,25 @@ public void Setup()
_base58Encoded = Base58.Bitcoin.Encode(_testData);
}

[Benchmark(Description = "Our Base58 Encode", Baseline = true)]
[BenchmarkCategory("Encode"), Benchmark(Description = "Our Base58 Encode", Baseline = true)]
public string Encode_OurBase58()
{
return Base58.Bitcoin.Encode(_testData);
}

[Benchmark(Description = "SimpleBase Base58 Encode")]
[BenchmarkCategory("Encode"), Benchmark(Description = "SimpleBase Base58 Encode")]
public string Encode_SimpleBase58()
{
return SimpleBase.Base58.Bitcoin.Encode(_testData);
}

[Benchmark(Description = "Our Base58 Decode")]
[BenchmarkCategory("Decode"), Benchmark(Description = "Our Base58 Decode", Baseline = true)]
public byte[] Decode_OurBase58()
{
return Base58.Bitcoin.Decode(_base58Encoded);
}

[Benchmark(Description = "SimpleBase Base58 Decode")]
[BenchmarkCategory("Decode"), Benchmark(Description = "SimpleBase Base58 Decode")]
public byte[] Decode_SimpleBase58()
{
return SimpleBase.Base58.Bitcoin.Decode(_base58Encoded);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- Emit PDBs even in Release so DisassemblyDiagnoser(printSource: true) can map asm to C#. -->
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="SimpleBase" />
<PackageReference Include="System.Numerics.Tensors" />
</ItemGroup>

<ItemGroup>
Expand Down
210 changes: 210 additions & 0 deletions src/Base58Encoding.Benchmarks/EndToEndDecodeBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace Base58Encoding.Benchmarks;

// End-to-end 64-byte decode A/B. Both arms run the identical full decode path (validate,
// intermediate build, matmul, carry-reduce, leading-zero check, byte output) and differ ONLY
// in the matmul step (SIMD TensorDot over transposed ulong columns vs jagged scalar). Measured
// in a single run, so the ratio is the real end-to-end effect of the matmul change.
[SimpleJob(RuntimeMoniker.Net10_0)]
[HideColumns("Error", "StdDev", "RatioSD")]
public class EndToEndDecodeBenchmark
{
private string _sig64 = default!;
private ulong[] _transposed64 = default!;
private uint[][] _jagged64 = default!;

[GlobalSetup]
public void Setup()
{
var bytes = new byte[64];
Random.Shared.NextBytes(bytes);
_sig64 = Base58.Bitcoin.Encode(bytes);

// Production tables on this branch are transposed column-major ulong.
_transposed64 = Base58BitcoinTables.DecodeTable64;

// Rebuild the original jagged row-major layout: jagged[i][j] = transposed[j*IntermediateSz64 + i].
int rows = Base58BitcoinTables.IntermediateSz64;
int cols = Base58BitcoinTables.BinarySz64;
_jagged64 = new uint[rows][];
for (int i = 0; i < rows; i++)
{
_jagged64[i] = new uint[cols];
for (int j = 0; j < cols; j++)
{
_jagged64[i][j] = (uint)_transposed64[j * rows + i];
}
}

// Correctness: both matmul variants must reproduce the production decode exactly.
Span<byte> a = stackalloc byte[64];
Span<byte> b = stackalloc byte[64];
DecodeFull64(_sig64, a, useSimd: true);
DecodeFull64(_sig64, b, useSimd: false);
byte[] expected = Base58.Bitcoin.Decode(_sig64);
if (!a.SequenceEqual(expected) || !b.SequenceEqual(expected))
{
throw new InvalidOperationException("DecodeFull64 does not match production Decode");
}
}

[Benchmark(Baseline = true)]
public int Full_Jagged()
{
Span<byte> dest = stackalloc byte[64];
return DecodeFull64(_sig64, dest, useSimd: false);
}

[Benchmark]
public int Full_Simd()
{
Span<byte> dest = stackalloc byte[64];
return DecodeFull64(_sig64, dest, useSimd: true);
}

[SkipLocalsInit]
private int DecodeFull64(ReadOnlySpan<char> encoded, Span<byte> destination, bool useSimd)
{
int charCount = encoded.Length;

Span<byte> rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64];
ReadOnlySpan<byte> decodeTable = BitcoinAlphabet.DecodeTable;

int prepend0 = Base58BitcoinTables.Raw58Sz64 - charCount;
for (int j = 0; j < Base58BitcoinTables.Raw58Sz64; j++)
{
if (j < prepend0)
{
rawBase58[j] = 0;
}
else
{
int c = encoded[j - prepend0];
if ((uint)c >= 128 || decodeTable[c] == 255)
{
ThrowHelper.ThrowInvalidCharacter((char)c);
}

rawBase58[j] = decodeTable[c];
}
}

Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64];
for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
{
intermediate[i] = (ulong)rawBase58[5 * i + 0] * 11316496UL +
(ulong)rawBase58[5 * i + 1] * 195112UL +
(ulong)rawBase58[5 * i + 2] * 3364UL +
(ulong)rawBase58[5 * i + 3] * 58UL +
(ulong)rawBase58[5 * i + 4] * 1UL;
}

Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz64];
if (useSimd)
{
var table = _transposed64;
int rows = Base58BitcoinTables.IntermediateSz64;
for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++)
{
binary[j] = TensorDot(intermediate, table.AsSpan(j * rows, rows));
}
}
else
{
var table = _jagged64;
for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++)
{
ulong acc = 0UL;
for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
{
acc += intermediate[i] * table[i][j];
}

binary[j] = acc;
}
}

for (int i = Base58BitcoinTables.BinarySz64 - 1; i > 0; i--)
{
binary[i - 1] += binary[i] >> 32;
binary[i] &= 0xFFFFFFFFUL;
}

if (binary[0] > 0xFFFFFFFFUL)
{
return -1;
}

int outputLeadingZeros = 0;
for (int i = 0; i < Base58BitcoinTables.BinarySz64; i++)
{
uint v = (uint)binary[i];
if (v != 0)
{
outputLeadingZeros += BitOperations.LeadingZeroCount(v) / 8;
break;
}
outputLeadingZeros += 4;
}

int inputLeadingOnes = Base58.CountLeadingCharacters(encoded, '1');
if (outputLeadingZeros != inputLeadingOnes)
{
return -1;
}

for (int i = 0; i < Base58BitcoinTables.BinarySz64; i++)
{
BinaryPrimitives.WriteUInt32BigEndian(destination.Slice(i * sizeof(uint), sizeof(uint)), (uint)binary[i]);
}

return 64;
}

private static ulong TensorDot(ReadOnlySpan<ulong> x, ReadOnlySpan<ulong> y)
{
ref ulong xr = ref MemoryMarshal.GetReference(x);
ref ulong yr = ref MemoryMarshal.GetReference(y);
int len = x.Length;
int i = 0;
ulong sum = 0UL;

if (Vector256.IsHardwareAccelerated && len >= Vector256<ulong>.Count)
{
Vector256<ulong> acc = Vector256<ulong>.Zero;
int upper = len - Vector256<ulong>.Count;
for (; i <= upper; i += Vector256<ulong>.Count)
{
acc += Vector256.LoadUnsafe(ref xr, (nuint)i) * Vector256.LoadUnsafe(ref yr, (nuint)i);
}

sum += Vector256.Sum(acc);
}
else if (Vector128.IsHardwareAccelerated && len >= Vector128<ulong>.Count)
{
Vector128<ulong> acc = Vector128<ulong>.Zero;
int upper = len - Vector128<ulong>.Count;
for (; i <= upper; i += Vector128<ulong>.Count)
{
acc += Vector128.LoadUnsafe(ref xr, (nuint)i) * Vector128.LoadUnsafe(ref yr, (nuint)i);
}

sum += Vector128.Sum(acc);
}

for (; i < len; i++)
{
sum += Unsafe.Add(ref xr, i) * Unsafe.Add(ref yr, i);
}

return sum;
}
}
Loading
Loading