Skip to content

Commit 4d66fbd

Browse files
authored
Merge pull request #1 from unsafePtr/feat/firedancer-optimizations
Feat/firedancer optimizations
2 parents 84f6201 + 02c7b64 commit 4d66fbd

29 files changed

+1905
-304
lines changed

.editorconfig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
end_of_line = lf
7+
charset = utf-8
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.cs]
12+
csharp_new_line_before_open_brace = all
13+
csharp_new_line_before_else = true
14+
csharp_new_line_before_catch = true
15+
csharp_new_line_before_finally = true
16+
17+
dotnet_separate_import_directive_groups = true
18+
dotnet_sort_system_usings_first = true

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
* text=auto
2+
3+
*.cs text diff=csharp

.github/workflows/publish-nuget.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ on:
2222
required: false
2323
type: string
2424

25+
permissions:
26+
id-token: write # required for GitHub OIDC
27+
2528
jobs:
2629
build:
2730
runs-on: ubuntu-latest
@@ -42,7 +45,7 @@ jobs:
4245
- name: Setup .NET
4346
uses: actions/setup-dotnet@v4
4447
with:
45-
dotnet-version: 9.0.x
48+
dotnet-version: 10.0.x
4649

4750
- name: Restore dependencies
4851
run: dotnet restore src/Base58Encoding.slnx
@@ -71,6 +74,7 @@ jobs:
7174
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.version != '')
7275
permissions:
7376
contents: write
77+
id-token: write # enable GitHub OIDC token issuance for this job
7478

7579
steps:
7680
- name: Download artifacts
@@ -80,12 +84,18 @@ jobs:
8084
path: ./artifacts
8185

8286
- name: Setup .NET
83-
uses: actions/setup-dotnet@v4
87+
uses: actions/setup-dotnet@v5
88+
with:
89+
dotnet-version: 10.0.x
90+
91+
- name: NuGet login (OIDC → temp API key)
92+
uses: NuGet/login@v1
93+
id: login
8494
with:
85-
dotnet-version: 9.0.x
95+
user: ${{ secrets.NUGET_USER }}
8696

8797
- name: Publish to NuGet
88-
run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate
98+
run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate
8999

90100
- name: Create GitHub Release
91101
uses: softprops/action-gh-release@v2

README.md

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Base58 Encoding Library
22

3-
A .NET 9.0 Base58 encoding and decoding library with support for multiple alphabet variants.
3+
A .NET 10.0 Base58 encoding and decoding library with support for multiple alphabet variants.
44

55
## Features
66

7-
- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui), Ripple, and Flickr alphabets
7+
- **Multiple Alphabets**: Built-in support for Bitcoin(IFPS/Sui/Solana), Ripple, and Flickr alphabets
88
- **Memory Efficient**: Uses stackalloc operations when possible to minimize allocations
99
- **Type Safe**: Leverages ReadOnlySpan and ReadOnlyMemory for safe memory operations
1010
- **Intrinsics**: Uses SIMD `Vector128/Vector256` and unrolled loop for counting leading zeros
11+
- **Optimized Hot Paths**: Fast fixed-length encode/decode for 32-byte and 64-byte inputs using Firedancer-like optimizations
1112

1213
## Usage
1314

@@ -24,50 +25,69 @@ byte[] decoded = Base58.Bitcoin.Decode(encoded);
2425
// Ripple / Flickr
2526
Base58.Ripple.Encode(data);
2627
Base58.Flickr.Encode(data);
28+
```
29+
30+
## Performance
31+
32+
The library automatically uses optimized fast paths for common fixed-size inputs:
33+
- **32-byte inputs** (Bitcoin/Solana addresses, SHA-256 hashes): 8.5x faster encoding
34+
- **64-byte inputs** (SHA-512 hashes): Similar performance improvements
35+
36+
These optimizations are based on Firedancer's specialized Base58 algorithms and are transparent to the user. Unlike Firedancer however, we fallback to the generic approach in case of edge-cases.
37+
38+
**Algorithm Details:**
39+
- Uses **Mixed Radix Conversion (MRC)** with intermediate base 58^5 representation
40+
- Precomputed multiplication tables replace expensive division operations
41+
- Converts binary data to base 58^5 limbs, then to raw base58 digits
42+
- Matrix multiplication approach processes 5 base58 digits simultaneously
43+
- Separate encode/decode tables for 32-byte and 64-byte fixed sizes
44+
- Achieves ~2.5x speedup through table-based optimizations vs iterative division
45+
46+
**References:**
47+
- [Firedancer C implementation](https://github.com/firedancer-io/firedancer/tree/main/src/ballet/base58)
2748

28-
// Custom
29-
new Base58(Base58Alphabet.Custom(""));
3049
```
3150
3251
## Benchmarks
3352
3453
```
35-
BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4946/24H2/2024Update/HudsonValley)
54+
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7462/25H2/2025Update/HudsonValley2)
3655
13th Gen Intel Core i7-13700KF 3.40GHz, 1 CPU, 24 logical and 16 physical cores
37-
.NET SDK 9.0.304
38-
[Host] : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
39-
.NET 9.0 : .NET 9.0.8 (9.0.825.36511), X64 RyuJIT AVX2
56+
.NET SDK 10.0.101
57+
[Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
58+
DefaultJob : .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3
59+
60+
Job=DefaultJob
4061

41-
Job=.NET 9.0 Runtime=.NET 9.0
4262
```
43-
| Method | VectorType | Mean | Error | StdDev | Median |
44-
|--------------------------- |--------------- |------------:|----------:|----------:|------------:|
45-
| **'Our Base58 Encode'** | **BitcoinAddress** | **519.2 ns** | **1.99 ns** | **1.76 ns** | **519.2 ns** |
46-
| 'SimpleBase Base58 Encode' | BitcoinAddress | 770.1 ns | 4.93 ns | 4.61 ns | 769.8 ns |
47-
| 'Our Base58 Decode' | BitcoinAddress | 187.5 ns | 2.06 ns | 1.93 ns | 187.8 ns |
48-
| 'SimpleBase Base58 Decode' | BitcoinAddress | 502.1 ns | 6.95 ns | 5.43 ns | 502.4 ns |
49-
| | | | | | |
50-
| **'Our Base58 Encode'** | **SolanaAddress** | **1,375.9 ns** | **36.83 ns** | **108.59 ns** | **1,410.7 ns** |
51-
| 'SimpleBase Base58 Encode' | SolanaAddress | 2,546.5 ns | 112.91 ns | 329.36 ns | 2,661.5 ns |
52-
| 'Our Base58 Decode' | SolanaAddress | 536.5 ns | 24.89 ns | 73.39 ns | 564.3 ns |
53-
| 'SimpleBase Base58 Decode' | SolanaAddress | 1,210.2 ns | 31.14 ns | 90.33 ns | 1,236.8 ns |
54-
| | | | | | |
55-
| **'Our Base58 Encode'** | **SolanaTx** | **4,185.4 ns** | **48.51 ns** | **37.87 ns** | **4,173.0 ns** |
56-
| 'SimpleBase Base58 Encode' | SolanaTx | 10,844.0 ns | 337.01 ns | 988.40 ns | 11,158.2 ns |
57-
| 'Our Base58 Decode' | SolanaTx | 2,159.0 ns | 116.99 ns | 344.95 ns | 2,294.0 ns |
58-
| 'SimpleBase Base58 Decode' | SolanaTx | 5,357.9 ns | 177.28 ns | 494.18 ns | 5,506.1 ns |
59-
| | | | | | |
60-
| **'Our Base58 Encode'** | **IPFSHash** | **1,285.9 ns** | **80.51 ns** | **237.37 ns** | **1,081.6 ns** |
61-
| 'SimpleBase Base58 Encode' | IPFSHash | 1,654.3 ns | 4.14 ns | 3.88 ns | 1,653.9 ns |
62-
| 'Our Base58 Decode' | IPFSHash | 347.0 ns | 1.19 ns | 0.99 ns | 347.0 ns |
63-
| 'SimpleBase Base58 Decode' | IPFSHash | 883.4 ns | 16.93 ns | 15.01 ns | 883.2 ns |
64-
| | | | | | |
65-
| **'Our Base58 Encode'** | **MoneroAddress** | **4,907.0 ns** | **13.36 ns** | **11.84 ns** | **4,907.9 ns** |
66-
| 'SimpleBase Base58 Encode' | MoneroAddress | 8,998.7 ns | 25.07 ns | 20.94 ns | 8,998.8 ns |
67-
| 'Our Base58 Decode' | MoneroAddress | 1,367.1 ns | 4.38 ns | 3.89 ns | 1,366.5 ns |
68-
| 'SimpleBase Base58 Decode' | MoneroAddress | 3,809.8 ns | 59.58 ns | 55.73 ns | 3,797.3 ns |
63+
| Method | VectorType | Mean | Ratio | Gen0 | Allocated | Alloc Ratio |
64+
|--------------------------- |--------------- |------------:|------:|-------:|----------:|------------:|
65+
| **'Our Base58 Encode'** | **BitcoinAddress** | **537.07 ns** | **1.00** | **0.0057** | **96 B** | **1.00** |
66+
| 'SimpleBase Base58 Encode' | BitcoinAddress | 782.31 ns | 1.46 | 0.0057 | 96 B | 1.00 |
67+
| 'Our Base58 Decode' | BitcoinAddress | 168.95 ns | 0.31 | 0.0033 | 56 B | 0.58 |
68+
| 'SimpleBase Base58 Decode' | BitcoinAddress | 352.63 ns | 0.66 | 0.0033 | 56 B | 0.58 |
69+
| | | | | | | |
70+
| **'Our Base58 Encode'** | **SolanaAddress** | **93.41 ns** | **1.00** | **0.0070** | **112 B** | **1.00** |
71+
| 'SimpleBase Base58 Encode' | SolanaAddress | 1,430.37 ns | 15.31 | 0.0057 | 112 B | 1.00 |
72+
| 'Our Base58 Decode' | SolanaAddress | 181.71 ns | 1.95 | 0.0035 | 56 B | 0.50 |
73+
| 'SimpleBase Base58 Decode' | SolanaAddress | 837.03 ns | 8.96 | 0.0019 | 56 B | 0.50 |
74+
| | | | | | | |
75+
| **'Our Base58 Encode'** | **SolanaTx** | **252.31 ns** | **1.00** | **0.0124** | **200 B** | **1.00** |
76+
| 'SimpleBase Base58 Encode' | SolanaTx | 7,247.09 ns | 28.73 | 0.0076 | 200 B | 1.00 |
77+
| 'Our Base58 Decode' | SolanaTx | 178.05 ns | 0.71 | 0.0055 | 88 B | 0.44 |
78+
| 'SimpleBase Base58 Decode' | SolanaTx | 2,379.54 ns | 9.43 | 0.0038 | 88 B | 0.44 |
79+
| | | | | | | |
80+
| **'Our Base58 Encode'** | **IPFSHash** | **1,096.58 ns** | **1.00** | **0.0076** | **120 B** | **1.00** |
81+
| 'SimpleBase Base58 Encode' | IPFSHash | 1,644.83 ns | 1.50 | 0.0076 | 120 B | 1.00 |
82+
| 'Our Base58 Decode' | IPFSHash | 287.87 ns | 0.26 | 0.0038 | 64 B | 0.53 |
83+
| 'SimpleBase Base58 Decode' | IPFSHash | 643.63 ns | 0.59 | 0.0038 | 64 B | 0.53 |
84+
| | | | | | | |
85+
| **'Our Base58 Encode'** | **MoneroAddress** | **4,998.35 ns** | **1.00** | **0.0076** | **216 B** | **1.00** |
86+
| 'SimpleBase Base58 Encode' | MoneroAddress | 8,585.92 ns | 1.72 | - | 216 B | 1.00 |
87+
| 'Our Base58 Decode' | MoneroAddress | 1,173.48 ns | 0.23 | 0.0057 | 96 B | 0.44 |
88+
| 'SimpleBase Base58 Decode' | MoneroAddress | 3,716.38 ns | 0.74 | 0.0038 | 96 B | 0.44 |
6989
7090
7191
## License
7292
73-
This project is available under the MIT License.
93+
This project is available under the MIT License.

src/Base58Encoding.Benchmarks/Base58ComparisonBenchmark.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1+
using Base58Encoding.Benchmarks.Common;
2+
13
using BenchmarkDotNet.Attributes;
2-
using BenchmarkDotNet.Columns;
34
using BenchmarkDotNet.Diagnosers;
4-
using BenchmarkDotNet.Jobs;
5-
using Base58Encoding.Benchmarks.Common;
65

76
namespace Base58Encoding.Benchmarks;
87

9-
[SimpleJob(RuntimeMoniker.Net90)]
10-
[SimpleJob(RuntimeMoniker.Net10_0)]
118
[MemoryDiagnoser]
12-
[DisassemblyDiagnoser(exportCombinedDisassemblyReport: true)]
139
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
1410
public class Base58ComparisonBenchmark
1511
{
@@ -55,4 +51,4 @@ public byte[] Decode_SimpleBase58()
5551
{
5652
return SimpleBase.Base58.Bitcoin.Decode(_base58Encoded);
5753
}
58-
}
54+
}

src/Base58Encoding.Benchmarks/Base58Encoding.Benchmarks.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
5+
<TargetFrameworks>net10.0</TargetFrameworks>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
13-
<PackageReference Include="SimpleBase" Version="5.4.1" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
13+
<PackageReference Include="SimpleBase" Version="5.6.0" />
1414
</ItemGroup>
1515

1616
<ItemGroup>

src/Base58Encoding.Benchmarks/BoundsCheckComparisonBenchmark.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
using BenchmarkDotNet.Attributes;
2-
using BenchmarkDotNet.Columns;
3-
using BenchmarkDotNet.Diagnosers;
4-
using BenchmarkDotNet.Jobs;
51
using System.Runtime.CompilerServices;
62
using System.Runtime.InteropServices;
3+
74
using Base58Encoding.Benchmarks.Common;
85

6+
using BenchmarkDotNet.Attributes;
7+
using BenchmarkDotNet.Diagnosers;
8+
using BenchmarkDotNet.Jobs;
9+
910
namespace Base58Encoding.Benchmarks;
1011

1112
[SimpleJob(RuntimeMoniker.Net90)]
@@ -17,7 +18,6 @@ public class BoundsCheckComparisonBenchmark
1718
private const int Base = 58;
1819
private const int MaxStackallocByte = 512;
1920
private byte[] _testData = null!;
20-
private Base58 _base58 = null!;
2121

2222
[Params(TestVectors.VectorType.BitcoinAddress, TestVectors.VectorType.SolanaAddress, TestVectors.VectorType.SolanaTx, TestVectors.VectorType.IPFSHash, TestVectors.VectorType.MoneroAddress, TestVectors.VectorType.FlickrTestData, TestVectors.VectorType.RippleTestData)]
2323
public TestVectors.VectorType VectorType { get; set; }
@@ -26,7 +26,6 @@ public class BoundsCheckComparisonBenchmark
2626
public void Setup()
2727
{
2828
_testData = TestVectors.GetVector(VectorType);
29-
_base58 = Base58.Bitcoin;
3029
}
3130

3231
[Benchmark(Baseline = true)]
@@ -146,4 +145,4 @@ public unsafe byte[] EncodeFixed()
146145
digits.Slice(0, digitCount).CopyTo(result);
147146
return result;
148147
}
149-
}
148+
}

src/Base58Encoding.Benchmarks/Common/TestVectors.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ public static byte[] GetVector(VectorType type)
3939
_ => BitcoinAddress
4040
};
4141
}
42-
}
42+
}

src/Base58Encoding.Benchmarks/CountLeadingCharactersBenchmark.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
using BenchmarkDotNet.Attributes;
2-
using BenchmarkDotNet.Columns;
3-
using BenchmarkDotNet.Jobs;
4-
using System.Collections.Generic;
51
using System.Runtime.CompilerServices;
62
using System.Runtime.InteropServices;
7-
using static System.Net.Mime.MediaTypeNames;
3+
4+
using BenchmarkDotNet.Attributes;
5+
using BenchmarkDotNet.Jobs;
86

97
namespace Base58Encoding.Benchmarks;
108

@@ -128,4 +126,4 @@ internal static int CountLeadingCharacters(ReadOnlySpan<char> text, char target)
128126

129127
return count;
130128
}
131-
}
129+
}

src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
using BenchmarkDotNet.Attributes;
2-
using BenchmarkDotNet.Columns;
3-
using BenchmarkDotNet.Jobs;
4-
using System.Collections.Generic;
51
using System.Numerics;
62
using System.Runtime.CompilerServices;
73
using System.Runtime.InteropServices;
84
using System.Runtime.Intrinsics;
95

6+
using BenchmarkDotNet.Attributes;
7+
using BenchmarkDotNet.Jobs;
8+
109
namespace Base58Encoding.Benchmarks;
1110

1211
[SimpleJob(RuntimeMoniker.Net90)]
@@ -219,4 +218,4 @@ private static int CountLeadingZerosCombinedImpl(ReadOnlySpan<byte> data)
219218

220219
return count + CountLeadingZerosScalarImpl(data.Slice(count));
221220
}
222-
}
221+
}

0 commit comments

Comments
 (0)