Skip to content

Commit 1ea0588

Browse files
authored
Merge pull request #3 from unsafePtr/feat/drop-vector128
feat: drop vector128 for counting zeros
2 parents 57cfa39 + 142fbbd commit 1ea0588

File tree

5 files changed

+62
-44
lines changed

5 files changed

+62
-44
lines changed

src/Base58Encoding.Tests/Base58DecodeFast.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,20 @@ public void Decode64Fast_WithAllZeros_WorksCorrectly()
7676
}
7777

7878
[Theory]
79-
[InlineData("invalid0chars")] // Invalid character '0'
80-
public void Decode32Fast_WithInvalidInput_ReturnsNull(string input)
79+
[InlineData("invalid")] // Invalid character 'l'
80+
[InlineData("0chars")] // Invalid character '0'
81+
public void Decode64Fast_WithInvalidInput_ReturnsNull(string input)
8182
{
82-
Assert.Null(Base58.DecodeBitcoin64Fast(input));
83+
Assert.Throws<ArgumentException>(() => Base58.DecodeBitcoin64Fast(input));
84+
}
85+
86+
[Theory]
87+
[InlineData("invalid")] // Invalid character 'l'
88+
[InlineData("0chars")] // Invalid character '0'
89+
90+
public void Decode32ast_WithInvalidInput_ReturnsNull(string input)
91+
{
92+
Assert.Throws<ArgumentException>(() => Base58.DecodeBitcoin32Fast(input));
8393
}
8494

8595
[Fact]

src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Runtime.Intrinsics;
2+
13
namespace Base58Encoding.Tests;
24

35
public class SimpleLeadingZerosTest
@@ -27,4 +29,38 @@ public void BitcoinAddress_CountLeadingZerosMultipleWays_SameResult()
2729

2830
Assert.Equal(simdScalarCount, manualCount);
2931
}
32+
33+
[Theory]
34+
[InlineData(5)]
35+
[InlineData(12)]
36+
[InlineData(13)]
37+
[InlineData(23)]
38+
[InlineData(31)]
39+
public void CountLeadingZeros_32Size_ReturnsCorrectNumber(int zerosCount)
40+
{
41+
// Arrange
42+
var data = new byte[32];
43+
data.AsSpan(0, zerosCount).Fill(0x00);
44+
Random.Shared.NextBytes(data.AsSpan(zerosCount));
45+
46+
// Act
47+
var result = Base58.CountLeadingZerosSimd(data, out var processed);
48+
49+
Assert.Equal(zerosCount, result);
50+
Assert.Equal(data.Length, processed);
51+
}
52+
53+
[Fact]
54+
public void CountLeadingZeros_512Size_ReturnsCorrectNumber()
55+
{
56+
// Arrange
57+
var zerosCount = 123;
58+
var data = new byte[512];
59+
data.AsSpan(0, zerosCount).Fill(0x00);
60+
Random.Shared.NextBytes(data.AsSpan(zerosCount));
61+
// Act
62+
var result = Base58.CountLeadingZerosSimd(data, out var processed);
63+
Assert.Equal(zerosCount, result);
64+
Assert.Equal(Vector256<byte>.Count * 4, processed); // Vector256 used 4 times
65+
}
3066
}

src/Base58Encoding/Base58.CountLeading.cs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,6 @@ internal static int CountLeadingZerosSimd(ReadOnlySpan<byte> data, out int proce
4545
length -= Vector256<byte>.Count;
4646
}
4747
}
48-
else if (Vector128.IsHardwareAccelerated && length >= Vector128<byte>.Count)
49-
{
50-
var zeroVector = Vector128<byte>.Zero;
51-
52-
while (length >= Vector128<byte>.Count)
53-
{
54-
var vector = Vector128.LoadUnsafe(ref searchSpace, (nuint)count);
55-
var comparison = Vector128.Equals(vector, zeroVector);
56-
uint mask = comparison.ExtractMostSignificantBits();
57-
58-
if (mask != ushort.MaxValue)
59-
{
60-
processed = count + Vector128<byte>.Count;
61-
return count + BitOperations.TrailingZeroCount(~mask);
62-
}
63-
64-
count += Vector128<byte>.Count;
65-
length -= Vector128<byte>.Count;
66-
}
67-
}
6848

6949
processed = count;
7050
return count;

src/Base58Encoding/Base58.Decode.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,16 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
114114
else
115115
{
116116
char c = encoded[j - prepend0];
117-
// Validate + convert using Bitcoin decode table (return null for invalid chars)
117+
// Validate + convert using Bitcoin decode table
118118
if (c >= 128 || bitcoinDecodeTable[c] == 255)
119-
return null;
119+
ThrowHelper.ThrowInvalidCharacter(c);
120120

121121
rawBase58[j] = bitcoinDecodeTable[c];
122122
}
123123
}
124124

125125
// Convert to intermediate format (base 58^5)
126-
Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; // 9 elements
126+
Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32];
127127

128128
for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++)
129129
{
@@ -135,7 +135,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
135135
}
136136

137137
// Convert to overcomplete base 2^32 using decode table
138-
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; // 8 elements
138+
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz32];
139139

140140
for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++)
141141
{
@@ -183,6 +183,8 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
183183
}
184184

185185
// Leading zeros in output must match leading '1's in input
186+
// might be edge case since base58 of 32bytes can be between 32 and 44 characters.
187+
// will be handled by generic decoder if lengths don't match
186188
if (outputLeadingZeros != inputLeadingOnes) return null;
187189

188190
// Return the full 32 bytes - the result should always be 32 bytes for 32-byte decode
@@ -194,7 +196,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
194196
int charCount = encoded.Length;
195197

196198
// Convert to raw base58 digits with validation + conversion in one pass
197-
Span<byte> rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64]; // 90 bytes
199+
Span<byte> rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz64];
198200
var bitcoinDecodeTable = Base58Alphabet.Bitcoin.DecodeTable.Span;
199201

200202
// Prepend zeros to make exactly Raw58Sz64 characters
@@ -208,16 +210,16 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
208210
else
209211
{
210212
char c = encoded[j - prepend0];
211-
// Validate + convert using Bitcoin decode table (return null for invalid chars)
213+
// Validate + convert using Bitcoin decode table
212214
if (c >= 128 || bitcoinDecodeTable[c] == 255)
213-
return null;
215+
ThrowHelper.ThrowInvalidCharacter(c);
214216

215217
rawBase58[j] = bitcoinDecodeTable[c];
216218
}
217219
}
218220

219221
// Convert to intermediate format (base 58^5)
220-
Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64]; // 18 elements
222+
Span<ulong> intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz64];
221223

222224
for (int i = 0; i < Base58BitcoinTables.IntermediateSz64; i++)
223225
{
@@ -229,7 +231,7 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
229231
}
230232

231233
// Convert to overcomplete base 2^32 using decode table
232-
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz64]; // 16 elements
234+
Span<ulong> binary = stackalloc ulong[Base58BitcoinTables.BinarySz64];
233235

234236
for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++)
235237
{
@@ -277,6 +279,8 @@ internal byte[] DecodeGeneric(ReadOnlySpan<char> encoded)
277279
}
278280

279281
// Leading zeros in output must match leading '1's in input
282+
// might be edge case since base58 of 64bytes can be between 64 and 88 characters.
283+
// will be handled by generic decoder if lengths don't match
280284
if (outputLeadingZeros != inputLeadingOnes) return null;
281285

282286
// Return the full 64 bytes - the result should always be 64 bytes for 64-byte decode

src/Base58Encoding/Base58.Encode.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ internal string EncodeGeneric(ReadOnlySpan<byte> data)
9393

9494
internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> data)
9595
{
96-
// Count leading zeros (needed for final output)
9796
int inLeadingZeros = CountLeadingZeros(data);
9897

9998
if (inLeadingZeros == data.Length)
@@ -140,8 +139,6 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> data)
140139
rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U);
141140
rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U);
142141
rawBase58[5 * i + 0] = (byte)(v / 11316496U);
143-
144-
// Continue processing all values
145142
}
146143

147144
// Count leading zeros in raw output
@@ -156,12 +153,9 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> data)
156153
Debug.Assert(skip >= 0, "rawLeadingZeros should always be >= inLeadingZeros by Base58 math");
157154
int outputLength = Base58BitcoinTables.Raw58Sz32 - skip;
158155

159-
// Create state for string.Create
160156
var state = new EncodeFastState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength);
161-
162157
return string.Create(outputLength, state, static (span, state) =>
163158
{
164-
// Fill leading '1's for input leading zeros
165159
if (state.InLeadingZeros > 0)
166160
{
167161
span[..state.InLeadingZeros].Fill('1');
@@ -181,7 +175,6 @@ internal static string EncodeBitcoin32Fast(ReadOnlySpan<byte> data)
181175

182176
private static string EncodeBitcoin64Fast(ReadOnlySpan<byte> data)
183177
{
184-
// Count leading zeros (needed for final output)
185178
int inLeadingZeros = CountLeadingZeros(data);
186179

187180
if (inLeadingZeros == data.Length)
@@ -242,7 +235,6 @@ private static string EncodeBitcoin64Fast(ReadOnlySpan<byte> data)
242235
rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U);
243236
rawBase58[5 * i + 0] = (byte)(v / 11316496U);
244237

245-
// Debug.Assert - ensure all values are valid Base58 digits (algorithm correctness check)
246238
Debug.Assert(rawBase58[5 * i + 0] < 58 && rawBase58[5 * i + 1] < 58 &&
247239
rawBase58[5 * i + 2] < 58 && rawBase58[5 * i + 3] < 58 &&
248240
rawBase58[5 * i + 4] < 58,
@@ -256,17 +248,13 @@ private static string EncodeBitcoin64Fast(ReadOnlySpan<byte> data)
256248
if (rawBase58[rawLeadingZeros] != 0) break;
257249
}
258250

259-
// Calculate skip and final length
260251
int skip = rawLeadingZeros - inLeadingZeros;
261252
Debug.Assert(skip >= 0, "rawLeadingZeros should always be >= inLeadingZeros by Base58 math");
262253
int outputLength = Base58BitcoinTables.Raw58Sz64 - skip;
263254

264-
// Create state for string.Create
265255
var state = new EncodeFastState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength);
266-
267256
return string.Create(outputLength, state, static (span, state) =>
268257
{
269-
// Fill leading '1's for input leading zeros
270258
if (state.InLeadingZeros > 0)
271259
{
272260
span[..state.InLeadingZeros].Fill('1');

0 commit comments

Comments
 (0)