From f87c7daae7b427fbf0edd763a0525dadf84bfc85 Mon Sep 17 00:00:00 2001 From: Nikolay Zdravkov Date: Tue, 21 Jul 2026 17:03:50 +0300 Subject: [PATCH 1/2] chore: retarget CountLeadingZeros tests to public API and add IndexOfAnyExcept benchmark --- .../CountLeadingZerosBenchmark.cs | 13 ++++++ .../SimpleLeadingZerosTest.cs | 43 ++++++++----------- src/Base58Encoding/Base58.CountLeading.cs | 1 + 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs b/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs index e0f4073..e25df86 100644 --- a/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs +++ b/src/Base58Encoding.Benchmarks/CountLeadingZerosBenchmark.cs @@ -104,6 +104,19 @@ public int Combined() return totalCount; } + [Benchmark(Description = "IndexOfAnyExcept")] + public int IndexOfAnyExceptScan() + { + var totalCount = 0; + foreach (var data in TestData) + { + var index = data.AsSpan().IndexOfAnyExcept((byte)0); + totalCount += index < 0 ? data.Length : index; + } + + return totalCount; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int CountLeadingZerosArray(ReadOnlySpan data) { diff --git a/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs b/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs index 7ef2cdb..71ed168 100644 --- a/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs +++ b/src/Base58Encoding.Tests/SimpleLeadingZerosTest.cs @@ -1,11 +1,9 @@ -using System.Runtime.Intrinsics; - namespace Base58Encoding.Tests; public class SimpleLeadingZerosTest { [Fact] - public void BitcoinAddress_CountLeadingZerosMultipleWays_SameResult() + public void BitcoinAddress_CountLeadingZeros_MatchesManualCount() { var address = "1111111111111111111114oLvT2"; var decoded = Base58.Bitcoin.Decode(address); @@ -18,16 +16,7 @@ public void BitcoinAddress_CountLeadingZerosMultipleWays_SameResult() manualCount++; } - // Test SIMD - int simdCount = Base58.CountLeadingZerosSimd(decoded, out int processed); - var simdScalarCount = 0; - if (simdCount >= processed) - { - int remaining = Base58.CountLeadingZerosScalar(decoded.AsSpan(simdCount)); - simdScalarCount = simdCount + remaining; - } - - Assert.Equal(simdScalarCount, manualCount); + Assert.Equal(manualCount, Base58.CountLeadingZeros(decoded)); } [Theory] @@ -38,33 +27,35 @@ public void BitcoinAddress_CountLeadingZerosMultipleWays_SameResult() [InlineData(31)] public void CountLeadingZeros_32Size_ReturnsCorrectNumber(int zerosCount) { - Assert.SkipUnless(Vector256.IsHardwareAccelerated, "Requires Vector256 hardware acceleration"); - // Arrange var data = new byte[32]; data.AsSpan(0, zerosCount).Fill(0x00); Random.Shared.NextBytes(data.AsSpan(zerosCount)); + data[zerosCount] = 1; - // Act - var result = Base58.CountLeadingZerosSimd(data, out var processed); - - Assert.Equal(zerosCount, result); - Assert.Equal(data.Length, processed); + // Act / Assert + Assert.Equal(zerosCount, Base58.CountLeadingZeros(data)); } [Fact] public void CountLeadingZeros_512Size_ReturnsCorrectNumber() { - Assert.SkipUnless(Vector256.IsHardwareAccelerated, "Requires Vector256 hardware acceleration"); - // Arrange var zerosCount = 123; var data = new byte[512]; data.AsSpan(0, zerosCount).Fill(0x00); Random.Shared.NextBytes(data.AsSpan(zerosCount)); - // Act - var result = Base58.CountLeadingZerosSimd(data, out var processed); - Assert.Equal(zerosCount, result); - Assert.Equal(Vector256.Count * 4, processed); // Vector256 used 4 times + data[zerosCount] = 1; + + // Act / Assert + Assert.Equal(zerosCount, Base58.CountLeadingZeros(data)); + } + + [Fact] + public void CountLeadingZeros_AllZeros_ReturnsLength() + { + var data = new byte[40]; + + Assert.Equal(data.Length, Base58.CountLeadingZeros(data)); } } diff --git a/src/Base58Encoding/Base58.CountLeading.cs b/src/Base58Encoding/Base58.CountLeading.cs index f3e4c47..40592d3 100644 --- a/src/Base58Encoding/Base58.CountLeading.cs +++ b/src/Base58Encoding/Base58.CountLeading.cs @@ -71,6 +71,7 @@ internal static int CountLeadingZerosScalar(ReadOnlySpan data) } return count; } + count += sizeof(ulong); length -= sizeof(ulong); } From 690b1f93d46ad487362b286b22f1c102fa5c25df Mon Sep 17 00:00:00 2001 From: Nikolay Zdravkov Date: Tue, 21 Jul 2026 17:23:54 +0300 Subject: [PATCH 2/2] chore: mark public Encode/Decode span parameters as scoped --- src/Base58Encoding/Base58.Decode.cs | 6 +++--- src/Base58Encoding/Base58.Encode.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Base58Encoding/Base58.Decode.cs b/src/Base58Encoding/Base58.Decode.cs index 572a606..1f0ed96 100644 --- a/src/Base58Encoding/Base58.Decode.cs +++ b/src/Base58Encoding/Base58.Decode.cs @@ -14,7 +14,7 @@ public sealed partial class Base58 /// Base58 encoded input. /// Decoded byte array. /// Invalid Base58 character. - public byte[] Decode(ReadOnlySpan encoded) + public byte[] Decode(scoped ReadOnlySpan encoded) { if (encoded.IsEmpty) { @@ -53,7 +53,7 @@ public byte[] Decode(ReadOnlySpan encoded) /// /// Thrown on invalid Base58 character or when is too small. /// - public int Decode(ReadOnlySpan encoded, Span destination) + public int Decode(scoped ReadOnlySpan encoded, scoped Span destination) { if (encoded.IsEmpty) { @@ -72,7 +72,7 @@ public int Decode(ReadOnlySpan encoded, Span destination) /// /// Thrown on invalid Base58 character or when is too small. /// - public int Decode(ReadOnlySpan encoded, Span destination) + public int Decode(scoped ReadOnlySpan encoded, scoped Span destination) { if (encoded.IsEmpty) { diff --git a/src/Base58Encoding/Base58.Encode.cs b/src/Base58Encoding/Base58.Encode.cs index 032dae8..0a23262 100644 --- a/src/Base58Encoding/Base58.Encode.cs +++ b/src/Base58Encoding/Base58.Encode.cs @@ -14,7 +14,7 @@ public sealed partial class Base58 /// /// Bytes to encode. /// Base58 encoded string. - public string Encode(ReadOnlySpan data) + public string Encode(scoped ReadOnlySpan data) { if (data.IsEmpty) { @@ -41,7 +41,7 @@ public string Encode(ReadOnlySpan data) /// Destination buffer for ASCII-encoded Base58 characters. /// Number of bytes written to . /// Thrown if is too small. - public int Encode(ReadOnlySpan data, Span destination) + public int Encode(scoped ReadOnlySpan data, scoped Span destination) { if (data.IsEmpty) {