diff --git a/src/Base58Encoding.Benchmarks/EndToEndDecodeBenchmark.cs b/src/Base58Encoding.Benchmarks/EndToEndDecodeBenchmark.cs index 86be123..63a9d88 100644 --- a/src/Base58Encoding.Benchmarks/EndToEndDecodeBenchmark.cs +++ b/src/Base58Encoding.Benchmarks/EndToEndDecodeBenchmark.cs @@ -18,7 +18,6 @@ namespace Base58Encoding.Benchmarks; public class EndToEndDecodeBenchmark { private string _sig64 = default!; - private ulong[] _transposed64 = default!; private uint[][] _jagged64 = default!; [GlobalSetup] @@ -28,10 +27,9 @@ public void Setup() Random.Shared.NextBytes(bytes); _sig64 = Base58.Bitcoin.Encode(bytes); - // Production tables on this branch are transposed column-major ulong. - _transposed64 = Base58BitcoinTables.DecodeTable64; - + // The production table is transposed column-major ulong. // Rebuild the original jagged row-major layout: jagged[i][j] = transposed[j*IntermediateSz64 + i]. + ReadOnlySpan transposed = Base58BitcoinTables.DecodeTable64; int rows = Base58BitcoinTables.IntermediateSz64; int cols = Base58BitcoinTables.BinarySz64; _jagged64 = new uint[rows][]; @@ -40,7 +38,7 @@ public void Setup() _jagged64[i] = new uint[cols]; for (int j = 0; j < cols; j++) { - _jagged64[i][j] = (uint)_transposed64[j * rows + i]; + _jagged64[i][j] = (uint)transposed[j * rows + i]; } } @@ -110,11 +108,11 @@ private int DecodeFull64(ReadOnlySpan encoded, Span destination, boo Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz64]; if (useSimd) { - var table = _transposed64; + ReadOnlySpan table = Base58BitcoinTables.DecodeTable64; int rows = Base58BitcoinTables.IntermediateSz64; for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++) { - binary[j] = TensorDot(intermediate, table.AsSpan(j * rows, rows)); + binary[j] = TensorDot(intermediate, table.Slice(j * rows, rows)); } } else diff --git a/src/Base58Encoding.Benchmarks/EndToEndEncodeBenchmark.cs b/src/Base58Encoding.Benchmarks/EndToEndEncodeBenchmark.cs index 2f56752..e445609 100644 --- a/src/Base58Encoding.Benchmarks/EndToEndEncodeBenchmark.cs +++ b/src/Base58Encoding.Benchmarks/EndToEndEncodeBenchmark.cs @@ -32,7 +32,6 @@ private interface IMatmulMode { } private byte[] _data = default!; private uint[][] _jagged = default!; - private ulong[] _rowMajor = default!; [GlobalSetup] public void Setup() @@ -41,19 +40,20 @@ public void Setup() _data = new byte[Size]; rng.NextBytes(_data); - // Scalar arm reads the production jagged tables directly; the vectorized arms read each - // encode row laid out contiguously as ulong. Row length is IntermediateSz - 1 (the matmul - // only touches columns 0..IntermediateSz-2, accumulating into intermediate[1..]). - _jagged = Size == 32 ? Base58BitcoinTables.EncodeTable32 : Base58BitcoinTables.EncodeTable64; + // The vectorized arms read the production row-major table, and the scalar arm a jagged copy of it. + // Row length is IntermediateSz - 1, because the matmul only touches columns 0..IntermediateSz-2. + // It accumulates into intermediate[1..]. int binarySz = Size == 32 ? Base58BitcoinTables.BinarySz32 : Base58BitcoinTables.BinarySz64; int rowLen = (Size == 32 ? Base58BitcoinTables.IntermediateSz32 : Base58BitcoinTables.IntermediateSz64) - 1; - _rowMajor = new ulong[binarySz * rowLen]; + ReadOnlySpan rowMajor = RowMajor(Size); + _jagged = new uint[binarySz][]; for (int i = 0; i < binarySz; i++) { + _jagged[i] = new uint[rowLen]; for (int j = 0; j < rowLen; j++) { - _rowMajor[i * rowLen + j] = _jagged[i][j]; + _jagged[i][j] = (uint)rowMajor[i * rowLen + j]; } } @@ -183,14 +183,14 @@ private void AccumulateRows( return; } - ulong[] rowMajor = _rowMajor; + ReadOnlySpan rowMajor = RowMajor(Size); Span target = intermediate.Slice(1, rowLen); if (typeof(TMode) == typeof(TensorMode)) { for (int i = startRow; i < endRow; i++) { - ReadOnlySpan row = rowMajor.AsSpan(i * rowLen, rowLen); + ReadOnlySpan row = rowMajor.Slice(i * rowLen, rowLen); // destination[k] = (row[k] * binary[i]) + target[k]; addend and destination are the // same span (allowed: they begin at the same location). TensorPrimitives.MultiplyAdd(row, (ulong)binary[i], target, target); @@ -202,11 +202,14 @@ private void AccumulateRows( // SimdMode for (int i = startRow; i < endRow; i++) { - ReadOnlySpan row = rowMajor.AsSpan(i * rowLen, rowLen); + ReadOnlySpan row = rowMajor.Slice(i * rowLen, rowLen); MultiplyAddSimd(row, binary[i], target); } } + private static ReadOnlySpan RowMajor(int size) => + size == 32 ? Base58BitcoinTables.EncodeTable32RowMajor : Base58BitcoinTables.EncodeTable64RowMajor; + // acc[k] += x[k] * scale, widest available width first, then a scalar tail / fallback. private static void MultiplyAddSimd(ReadOnlySpan x, ulong scale, Span acc) { diff --git a/src/Base58Encoding.Benchmarks/JaggedVsMultidimensionalArrayBenchmark.cs b/src/Base58Encoding.Benchmarks/JaggedVsMultidimensionalArrayBenchmark.cs deleted file mode 100644 index d8ed15a..0000000 --- a/src/Base58Encoding.Benchmarks/JaggedVsMultidimensionalArrayBenchmark.cs +++ /dev/null @@ -1,414 +0,0 @@ -using System.Buffers.Binary; - -using BenchmarkDotNet.Attributes; - -namespace Base58Encoding.Benchmarks; - -/// -/// Benchmark comparing jagged arrays (uint[][]) vs multidimensional arrays (uint[,]) -/// for Base58 table lookup performance using the same Fast32 encode/decode logic -/// Verdict - on encoding both are the same, but on decoding jagged arrays are 5-10% faster. -/// -[MemoryDiagnoser] -public class JaggedVsMultidimensionalArrayBenchmark -{ - private byte[] _data = default!; - private string _encodedBase58 = default!; - - // Jagged arrays (current implementation) - private static readonly uint[][] JaggedEncodeTable32 = Base58BitcoinTables.EncodeTable32; - private static readonly uint[][] JaggedDecodeTable32 = UntransposeToJagged(Base58BitcoinTables.DecodeTable32, Base58BitcoinTables.IntermediateSz32, Base58BitcoinTables.BinarySz32); - - // Multidimensional arrays (alternative implementation) - private static readonly uint[,] MultidimensionalEncodeTable32 = ConvertToMultidimensional(Base58BitcoinTables.EncodeTable32); - private static readonly uint[,] MultidimensionalDecodeTable32 = ConvertToMultidimensional(JaggedDecodeTable32); - - private readonly ref struct FastEncodeState - { - public readonly ReadOnlySpan RawBase58; - public readonly int InLeadingZeros; - public readonly int RawLeadingZeros; - public readonly int OutputLength; - - public FastEncodeState(ReadOnlySpan rawBase58, int inLeadingZeros, int rawLeadingZeros, int outputLength) - { - RawBase58 = rawBase58; - InLeadingZeros = inLeadingZeros; - RawLeadingZeros = rawLeadingZeros; - OutputLength = outputLength; - } - } - - [GlobalSetup] - public void Setup() - { - _data = new byte[32]; - Random.Shared.NextBytes(_data); - _encodedBase58 = SimpleBase.Base58.Bitcoin.Encode(_data); - } - - [Benchmark(Baseline = true)] - public string EncodeWithJaggedArray() - { - return EncodeBitcoin32FastJagged(_data); - } - - [Benchmark] - public string EncodeWithMultidimensionalArray() - { - return EncodeBitcoin32FastMultidimensional(_data); - } - - [Benchmark] - public byte[] DecodeWithJaggedArray() - { - return DecodeBitcoin32FastJagged(_encodedBase58)!; - } - - [Benchmark] - public byte[] DecodeWithMultidimensionalArray() - { - return DecodeBitcoin32FastMultidimensional(_encodedBase58)!; - } - - // Production DecodeTable32 is now transposed column-major ulong; rebuild the original - // row-major jagged form so this layout comparison keeps compiling. - private static uint[][] UntransposeToJagged(ulong[] transposed, int rows, int cols) - { - var jagged = new uint[rows][]; - for (int i = 0; i < rows; i++) - { - jagged[i] = new uint[cols]; - for (int j = 0; j < cols; j++) - { - jagged[i][j] = (uint)transposed[j * rows + i]; - } - } - - return jagged; - } - - private static uint[,] ConvertToMultidimensional(uint[][] jaggedArray) - { - int rows = jaggedArray.Length; - int cols = jaggedArray[0].Length; - var result = new uint[rows, cols]; - - for (int i = 0; i < rows; i++) - { - for (int j = 0; j < cols; j++) - { - result[i, j] = jaggedArray[i][j]; - } - } - - return result; - } - - private static string EncodeBitcoin32FastJagged(ReadOnlySpan data) - { - // Count leading zeros - int inLeadingZeros = Base58.CountLeadingZeros(data); - - if (inLeadingZeros == data.Length) - { - return new string('1', inLeadingZeros); - } - - // Convert 32 bytes to 8 uint32 limbs (big-endian) - Span binary = stackalloc uint[Base58BitcoinTables.BinarySz32]; - for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) - { - int offset = i * sizeof(uint); - binary[i] = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, sizeof(uint))); - } - - // Convert to intermediate format (base 58^5) using JAGGED ARRAY - Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; - intermediate.Clear(); - - for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) - { - for (int j = 0; j < Base58BitcoinTables.IntermediateSz32 - 1; j++) - { - intermediate[j + 1] += (ulong)binary[i] * JaggedEncodeTable32[i][j]; - } - } - - // Reduce each term to be less than 58^5 - for (int i = Base58BitcoinTables.IntermediateSz32 - 1; i > 0; i--) - { - intermediate[i - 1] += intermediate[i] / Base58BitcoinTables.R1Div; - intermediate[i] %= Base58BitcoinTables.R1Div; - } - - // Convert intermediate form to raw base58 digits - Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32]; - for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++) - { - uint v = (uint)intermediate[i]; - rawBase58[5 * i + 4] = (byte)((v / 1U) % 58U); - rawBase58[5 * i + 3] = (byte)((v / 58U) % 58U); - rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U); - rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U); - rawBase58[5 * i + 0] = (byte)(v / 11316496U); - } - - // Count leading zeros in raw output - int rawLeadingZeros = 0; - for (; rawLeadingZeros < Base58BitcoinTables.Raw58Sz32; rawLeadingZeros++) - { - if (rawBase58[rawLeadingZeros] != 0) break; - } - - // Calculate skip and final length - int skip = rawLeadingZeros - inLeadingZeros; - int outputLength = Base58BitcoinTables.Raw58Sz32 - skip; - var state = new FastEncodeState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength); - return string.Create(outputLength, state, static (span, state) => - { - if (state.InLeadingZeros > 0) - { - span[..state.InLeadingZeros].Fill('1'); - } - - var bitcoinChars = Base58BitcoinTables.BitcoinChars; - for (int i = 0; i < state.OutputLength - state.InLeadingZeros; i++) - { - byte digit = state.RawBase58[state.RawLeadingZeros + i]; - span[state.InLeadingZeros + i] = (char)bitcoinChars[digit]; - } - }); - } - - private static string EncodeBitcoin32FastMultidimensional(ReadOnlySpan data) - { - // Count leading zeros - int inLeadingZeros = Base58.CountLeadingZeros(data); - - if (inLeadingZeros == data.Length) - { - return new string('1', inLeadingZeros); - } - - // Convert 32 bytes to 8 uint32 limbs (big-endian) - Span binary = stackalloc uint[Base58BitcoinTables.BinarySz32]; - for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) - { - int offset = i * sizeof(uint); - binary[i] = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(offset, sizeof(uint))); - } - - // Convert to intermediate format (base 58^5) using MULTIDIMENSIONAL ARRAY - Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; - intermediate.Clear(); - - for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) - { - for (int j = 0; j < Base58BitcoinTables.IntermediateSz32 - 1; j++) - { - intermediate[j + 1] += (ulong)binary[i] * MultidimensionalEncodeTable32[i, j]; - } - } - - // Reduce each term to be less than 58^5 - for (int i = Base58BitcoinTables.IntermediateSz32 - 1; i > 0; i--) - { - intermediate[i - 1] += intermediate[i] / Base58BitcoinTables.R1Div; - intermediate[i] %= Base58BitcoinTables.R1Div; - } - - // Convert intermediate form to raw base58 digits - Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32]; - for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++) - { - uint v = (uint)intermediate[i]; - rawBase58[5 * i + 4] = (byte)((v / 1U) % 58U); - rawBase58[5 * i + 3] = (byte)((v / 58U) % 58U); - rawBase58[5 * i + 2] = (byte)((v / 3364U) % 58U); - rawBase58[5 * i + 1] = (byte)((v / 195112U) % 58U); - rawBase58[5 * i + 0] = (byte)(v / 11316496U); - } - - // Count leading zeros in raw output - int rawLeadingZeros = 0; - for (; rawLeadingZeros < Base58BitcoinTables.Raw58Sz32; rawLeadingZeros++) - { - if (rawBase58[rawLeadingZeros] != 0) break; - } - - // Calculate skip and final length - int skip = rawLeadingZeros - inLeadingZeros; - int outputLength = Base58BitcoinTables.Raw58Sz32 - skip; - - var state = new FastEncodeState(rawBase58, inLeadingZeros, rawLeadingZeros, outputLength); - return string.Create(outputLength, state, static (span, state) => - { - if (state.InLeadingZeros > 0) - { - span[..state.InLeadingZeros].Fill('1'); - } - - var bitcoinChars = Base58BitcoinTables.BitcoinChars; - for (int i = 0; i < state.OutputLength - state.InLeadingZeros; i++) - { - byte digit = state.RawBase58[state.RawLeadingZeros + i]; - span[state.InLeadingZeros + i] = (char)bitcoinChars[digit]; - } - }); - } - - private static byte[]? DecodeBitcoin32FastJagged(string encoded) - { - // Early validation and length check - if (encoded.Length > Base58BitcoinTables.Raw58Sz32) return null; - - // Validate characters and create raw array using JAGGED ARRAY lookup - Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32]; - var bitcoinDecodeTable = BitcoinAlphabet.DecodeTable; - - int prepend0 = Base58BitcoinTables.Raw58Sz32 - encoded.Length; - for (int j = 0; j < Base58BitcoinTables.Raw58Sz32; j++) - { - if (j < prepend0) - { - rawBase58[j] = 0; - } - else - { - char c = encoded[j - prepend0]; - if (c >= 128 || bitcoinDecodeTable[c] == 255) - return null; - - rawBase58[j] = bitcoinDecodeTable[c]; - } - } - - // Convert to intermediate format - Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; - for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; 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; - } - - // Convert to binary using JAGGED ARRAY - Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; - for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++) - { - ulong acc = 0UL; - for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++) - { - acc += intermediate[i] * JaggedDecodeTable32[i][j]; - } - binary[j] = acc; - } - - // Reduce to proper uint32 values - for (int i = Base58BitcoinTables.BinarySz32 - 1; i > 0; i--) - { - binary[i - 1] += binary[i] >> 32; - binary[i] &= 0xFFFFFFFFUL; - } - - if (binary[0] > 0xFFFFFFFFUL) return null; - - // Convert to output bytes - var result = new byte[32]; - for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) - { - BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(i * 4, 4), (uint)binary[i]); - } - - // Validate leading zeros match leading '1's - int leadingZeroCnt = 0; - for (; leadingZeroCnt < 32; leadingZeroCnt++) - { - if (result[leadingZeroCnt] != 0) break; - if (encoded.Length <= leadingZeroCnt || encoded[leadingZeroCnt] != '1') return null; - } - if (leadingZeroCnt < encoded.Length && encoded[leadingZeroCnt] == '1') return null; - - return result; - } - - private static byte[]? DecodeBitcoin32FastMultidimensional(string encoded) - { - // Early validation and length check - if (encoded.Length > Base58BitcoinTables.Raw58Sz32) return null; - - // Validate characters and create raw array using MULTIDIMENSIONAL ARRAY lookup - Span rawBase58 = stackalloc byte[Base58BitcoinTables.Raw58Sz32]; - var bitcoinDecodeTable = BitcoinAlphabet.DecodeTable; - - int prepend0 = Base58BitcoinTables.Raw58Sz32 - encoded.Length; - for (int j = 0; j < Base58BitcoinTables.Raw58Sz32; j++) - { - if (j < prepend0) - { - rawBase58[j] = 0; - } - else - { - char c = encoded[j - prepend0]; - if (c >= 128 || bitcoinDecodeTable[c] == 255) - return null; - - rawBase58[j] = bitcoinDecodeTable[c]; - } - } - - // Convert to intermediate format - Span intermediate = stackalloc ulong[Base58BitcoinTables.IntermediateSz32]; - for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; 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; - } - - // Convert to binary using MULTIDIMENSIONAL ARRAY - Span binary = stackalloc ulong[Base58BitcoinTables.BinarySz32]; - for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++) - { - ulong acc = 0UL; - for (int i = 0; i < Base58BitcoinTables.IntermediateSz32; i++) - { - acc += intermediate[i] * MultidimensionalDecodeTable32[i, j]; - } - binary[j] = acc; - } - - // Reduce to proper uint32 values - for (int i = Base58BitcoinTables.BinarySz32 - 1; i > 0; i--) - { - binary[i - 1] += binary[i] >> 32; - binary[i] &= 0xFFFFFFFFUL; - } - - if (binary[0] > 0xFFFFFFFFUL) return null; - - // Convert to output bytes - var result = new byte[32]; - for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) - { - BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan(i * 4, 4), (uint)binary[i]); - } - - // Validate leading zeros match leading '1's - int leadingZeroCnt = 0; - for (; leadingZeroCnt < 32; leadingZeroCnt++) - { - if (result[leadingZeroCnt] != 0) break; - if (encoded.Length <= leadingZeroCnt || encoded[leadingZeroCnt] != '1') return null; - } - if (leadingZeroCnt < encoded.Length && encoded[leadingZeroCnt] == '1') return null; - - return result; - } -} diff --git a/src/Base58Encoding.Tests/Base58EncodeFast.cs b/src/Base58Encoding.Tests/Base58EncodeFast.cs index 56a0584..4d0486f 100644 --- a/src/Base58Encoding.Tests/Base58EncodeFast.cs +++ b/src/Base58Encoding.Tests/Base58EncodeFast.cs @@ -23,10 +23,10 @@ public void Encode32Fast_TableDimensions_AreCorrect() Assert.Equal(9, Base58BitcoinTables.IntermediateSz32); Assert.Equal(45, Base58BitcoinTables.Raw58Sz32); - // Verify table bounds - var encodeTable = Base58BitcoinTables.EncodeTable32; - Assert.Equal(8, encodeTable.Length); // Should be BinarySz32 - Assert.Equal(8, encodeTable[0].Length); // Should be IntermediateSz32 - 1 + // Encode has a row of IntermediateSz32 - 1 coefficients per source limb. + // Decode has IntermediateSz32 coefficients per output limb. + Assert.Equal(8 * 8, Base58BitcoinTables.EncodeTable32RowMajor.Length); + Assert.Equal(8 * 9, Base58BitcoinTables.DecodeTable32.Length); } [Fact] @@ -155,10 +155,10 @@ public void Encode64Fast_TableDimensions_AreCorrect() Assert.Equal(18, Base58BitcoinTables.IntermediateSz64); Assert.Equal(90, Base58BitcoinTables.Raw58Sz64); - // Verify table bounds - var encodeTable = Base58BitcoinTables.EncodeTable64; - Assert.Equal(16, encodeTable.Length); // Should be BinarySz64 - Assert.Equal(17, encodeTable[0].Length); // Should be IntermediateSz64 - 1 + // Encode has a row of IntermediateSz64 - 1 coefficients per source limb. + // Decode has IntermediateSz64 coefficients per output limb. + Assert.Equal(16 * 17, Base58BitcoinTables.EncodeTable64RowMajor.Length); + Assert.Equal(16 * 18, Base58BitcoinTables.DecodeTable64.Length); } [Fact] diff --git a/src/Base58Encoding.Tests/VectorMathTests.cs b/src/Base58Encoding.Tests/VectorMathTests.cs index 6ee03eb..fc41615 100644 --- a/src/Base58Encoding.Tests/VectorMathTests.cs +++ b/src/Base58Encoding.Tests/VectorMathTests.cs @@ -147,24 +147,23 @@ public void Kernels_HandleBoundaryOperands(ulong a, ulong b) public void MultiplyWidening32Precondition_HoldsForAllProductionOperands() { // Table side. - foreach ((string name, ulong[] table) in new (string, ulong[])[] - { - (nameof(Base58BitcoinTables.EncodeTable32RowMajor), Base58BitcoinTables.EncodeTable32RowMajor), - (nameof(Base58BitcoinTables.EncodeTable64RowMajor), Base58BitcoinTables.EncodeTable64RowMajor), - (nameof(Base58BitcoinTables.DecodeTable32), Base58BitcoinTables.DecodeTable32), - (nameof(Base58BitcoinTables.DecodeTable64), Base58BitcoinTables.DecodeTable64), - }) - { - for (int i = 0; i < table.Length; i++) - { - Assert.True(table[i] < (ulong)OperandLimit, - $"{name}[{i}] = {table[i]} exceeds 2^32; MultiplyWidening32 would truncate it."); - } - } + AssertOperandsFit(nameof(Base58BitcoinTables.EncodeTable32RowMajor), Base58BitcoinTables.EncodeTable32RowMajor); + AssertOperandsFit(nameof(Base58BitcoinTables.EncodeTable64RowMajor), Base58BitcoinTables.EncodeTable64RowMajor); + AssertOperandsFit(nameof(Base58BitcoinTables.DecodeTable32), Base58BitcoinTables.DecodeTable32); + AssertOperandsFit(nameof(Base58BitcoinTables.DecodeTable64), Base58BitcoinTables.DecodeTable64); // Limb side: an intermediate limb is at most 58^5 - 1, which is below 2^32. ulong maxLimb = (57UL * 11316496UL) + (57UL * 195112UL) + (57UL * 3364UL) + (57UL * 58UL) + 57UL; Assert.Equal(Base58BitcoinTables.R1Div - 1UL, maxLimb); Assert.True(maxLimb < (ulong)OperandLimit); } + + private static void AssertOperandsFit(string name, ReadOnlySpan table) + { + for (int i = 0; i < table.Length; i++) + { + Assert.True(table[i] < (ulong)OperandLimit, + $"{name}[{i}] = {table[i]} exceeds 2^32; MultiplyWidening32 would truncate it."); + } + } } diff --git a/src/Base58Encoding/Base58.Decode.cs b/src/Base58Encoding/Base58.Decode.cs index b71121f..fd1446e 100644 --- a/src/Base58Encoding/Base58.Decode.cs +++ b/src/Base58Encoding/Base58.Decode.cs @@ -297,7 +297,7 @@ internal static bool DecodeBitcoin32Fast(ReadOnlySpan encoded, Spa for (int j = 0; j < Base58BitcoinTables.BinarySz32; j++) { - binary[j] = VectorMath.TensorDot(intermediate, Base58BitcoinTables.DecodeTable32.AsSpan(j * Base58BitcoinTables.IntermediateSz32, Base58BitcoinTables.IntermediateSz32)); + binary[j] = VectorMath.TensorDot(intermediate, Base58BitcoinTables.DecodeTable32.Slice(j * Base58BitcoinTables.IntermediateSz32, Base58BitcoinTables.IntermediateSz32)); } // Reduce each term to less than 2^32 @@ -396,7 +396,7 @@ internal static bool DecodeBitcoin64Fast(ReadOnlySpan encoded, Spa for (int j = 0; j < Base58BitcoinTables.BinarySz64; j++) { - binary[j] = VectorMath.TensorDot(intermediate, Base58BitcoinTables.DecodeTable64.AsSpan(j * Base58BitcoinTables.IntermediateSz64, Base58BitcoinTables.IntermediateSz64)); + binary[j] = VectorMath.TensorDot(intermediate, Base58BitcoinTables.DecodeTable64.Slice(j * Base58BitcoinTables.IntermediateSz64, Base58BitcoinTables.IntermediateSz64)); } // Reduce each term to less than 2^32 diff --git a/src/Base58Encoding/Base58.Encode.cs b/src/Base58Encoding/Base58.Encode.cs index 285ba4a..286a5cb 100644 --- a/src/Base58Encoding/Base58.Encode.cs +++ b/src/Base58Encoding/Base58.Encode.cs @@ -265,7 +265,7 @@ private static int ComputeBitcoin32FastRaw(ReadOnlySpan data, Span r Span acc = intermediate.Slice(1, rowLen); for (int i = 0; i < Base58BitcoinTables.BinarySz32; i++) { - VectorMath.TensorMultiplyAdd(Base58BitcoinTables.EncodeTable32RowMajor.AsSpan(i * rowLen, rowLen), binary[i], acc); + VectorMath.TensorMultiplyAdd(Base58BitcoinTables.EncodeTable32RowMajor.Slice(i * rowLen, rowLen), binary[i], acc); } // Reduce each term to be less than 58^5 @@ -378,7 +378,7 @@ private static int ComputeBitcoin64FastRaw(ReadOnlySpan data, Span r Span acc = intermediate.Slice(1, rowLen); for (int i = 0; i < 8; i++) { - VectorMath.TensorMultiplyAdd(Base58BitcoinTables.EncodeTable64RowMajor.AsSpan(i * rowLen, rowLen), binary[i], acc); + VectorMath.TensorMultiplyAdd(Base58BitcoinTables.EncodeTable64RowMajor.Slice(i * rowLen, rowLen), binary[i], acc); } // Mini-reduction to prevent overflow (like Firedancer) @@ -387,7 +387,7 @@ private static int ComputeBitcoin64FastRaw(ReadOnlySpan data, Span r for (int i = 8; i < Base58BitcoinTables.BinarySz64; i++) { - VectorMath.TensorMultiplyAdd(Base58BitcoinTables.EncodeTable64RowMajor.AsSpan(i * rowLen, rowLen), binary[i], acc); + VectorMath.TensorMultiplyAdd(Base58BitcoinTables.EncodeTable64RowMajor.Slice(i * rowLen, rowLen), binary[i], acc); } // Reduce each term to be less than 58^5 diff --git a/src/Base58Encoding/Base58BitcoinTables.cs b/src/Base58Encoding/Base58BitcoinTables.cs index 4c73b86..a4942fe 100644 --- a/src/Base58Encoding/Base58BitcoinTables.cs +++ b/src/Base58Encoding/Base58BitcoinTables.cs @@ -1,7 +1,7 @@ -using System; - namespace Base58Encoding; +// The Firedancer tables are ReadOnlySpan properties over constant data in the assembly, not arrays. +// Nothing can modify them, no static constructor builds them, and verification tools can read them from the compiled code. internal static class Base58BitcoinTables { internal const uint R1Div = 656356768U; // 58^5 @@ -16,29 +16,23 @@ internal static class Base58BitcoinTables internal const int IntermediateSz32 = 9; internal const int Raw58Sz32 = IntermediateSz32 * 5; // 45 - // Contains the unique values less than 58^5 such that: - // 2^(32*(7-j)) = sum_k EncodeTable32[j][k]*58^(5*(7-k)) - internal static readonly uint[][] EncodeTable32 = + // Row j holds the unique values below 58^5 such that 2^(32*(7-j)) = sum_k row[k]*58^(5*(7-k)). + // Rows are contiguous because TensorMultiplyAdd vector-loads a whole source-limb row. + internal static ReadOnlySpan EncodeTable32RowMajor => [ - [513735U, 77223048U, 437087610U, 300156666U, 605448490U, 214625350U, 141436834U, 379377856U], - [0U, 78508U, 646269101U, 118408823U, 91512303U, 209184527U, 413102373U, 153715680U], - [0U, 0U, 11997U, 486083817U, 3737691U, 294005210U, 247894721U, 289024608U], - [0U, 0U, 0U, 1833U, 324463681U, 385795061U, 551597588U, 21339008U], - [0U, 0U, 0U, 0U, 280U, 127692781U, 389432875U, 357132832U], - [0U, 0U, 0U, 0U, 0U, 42U, 537767569U, 410450016U], - [0U, 0U, 0U, 0U, 0U, 0U, 6U, 356826688U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U] + 513735U, 77223048U, 437087610U, 300156666U, 605448490U, 214625350U, 141436834U, 379377856U, + 0U, 78508U, 646269101U, 118408823U, 91512303U, 209184527U, 413102373U, 153715680U, + 0U, 0U, 11997U, 486083817U, 3737691U, 294005210U, 247894721U, 289024608U, + 0U, 0U, 0U, 1833U, 324463681U, 385795061U, 551597588U, 21339008U, + 0U, 0U, 0U, 0U, 280U, 127692781U, 389432875U, 357132832U, + 0U, 0U, 0U, 0U, 0U, 42U, 537767569U, 410450016U, + 0U, 0U, 0U, 0U, 0U, 0U, 6U, 356826688U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U, ]; - // EncodeTable32 flattened to a contiguous row-major ulong[]: TensorMultiplyAdd vector-loads a - // whole source-limb row at once, so rows must be contiguous, and ulong matches the accumulator / - // vector element type (no per-element widening). Same values as the jagged table, via Flatten. - internal static readonly ulong[] EncodeTable32RowMajor = Flatten(EncodeTable32, IntermediateSz32 - 1); - - // Decode coefficients stored column-major (transposed): DecodeTable32[j * IntermediateSz32 + i] - // holds the coefficient for output limb j, intermediate limb i. Each output limb's column of - // IntermediateSz32 coefficients is contiguous, so it can be fed straight into TensorDot. - internal static readonly ulong[] DecodeTable32 = + // Element j * IntermediateSz32 + i is the coefficient of intermediate limb i in output limb j. + // Each output limb's coefficients are contiguous, so they feed straight into TensorDot. + internal static ReadOnlySpan DecodeTable32 => [ 1277U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 2650397687U, 8360U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, @@ -55,37 +49,31 @@ internal static class Base58BitcoinTables internal const int IntermediateSz64 = 18; internal const int Raw58Sz64 = IntermediateSz64 * 5; // 90 - // Contains the unique values less than 58^5 such that: - // 2^(32*(15-j)) = sum_k EncodeTable64[j][k]*58^(5*(17-k)) - internal static readonly uint[][] EncodeTable64 = + // Row j holds the unique values below 58^5 such that 2^(32*(15-j)) = sum_k row[k]*58^(5*(17-k)). + // Rows are contiguous because TensorMultiplyAdd vector-loads a whole source-limb row. + internal static ReadOnlySpan EncodeTable64RowMajor => [ - [2631U, 149457141U, 577092685U, 632289089U, 81912456U, 221591423U, 502967496U, 403284731U, 377738089U, 492128779U, 746799U, 366351977U, 190199623U, 38066284U, 526403762U, 650603058U, 454901440U], - [0U, 402U, 68350375U, 30641941U, 266024478U, 208884256U, 571208415U, 337765723U, 215140626U, 129419325U, 480359048U, 398051646U, 635841659U, 214020719U, 136986618U, 626219915U, 49699360U], - [0U, 0U, 61U, 295059608U, 141201404U, 517024870U, 239296485U, 527697587U, 212906911U, 453637228U, 467589845U, 144614682U, 45134568U, 184514320U, 644355351U, 104784612U, 308625792U], - [0U, 0U, 0U, 9U, 256449755U, 500124311U, 479690581U, 372802935U, 413254725U, 487877412U, 520263169U, 176791855U, 78190744U, 291820402U, 74998585U, 496097732U, 59100544U], - [0U, 0U, 0U, 0U, 1U, 285573662U, 455976778U, 379818553U, 100001224U, 448949512U, 109507367U, 117185012U, 347328982U, 522665809U, 36908802U, 577276849U, 64504928U], - [0U, 0U, 0U, 0U, 0U, 0U, 143945778U, 651677945U, 281429047U, 535878743U, 264290972U, 526964023U, 199595821U, 597442702U, 499113091U, 424550935U, 458949280U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 21997789U, 294590275U, 148640294U, 595017589U, 210481832U, 404203788U, 574729546U, 160126051U, 430102516U, 44963712U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 3361701U, 325788598U, 30977630U, 513969330U, 194569730U, 164019635U, 136596846U, 626087230U, 503769920U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 513735U, 77223048U, 437087610U, 300156666U, 605448490U, 214625350U, 141436834U, 379377856U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 78508U, 646269101U, 118408823U, 91512303U, 209184527U, 413102373U, 153715680U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 11997U, 486083817U, 3737691U, 294005210U, 247894721U, 289024608U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1833U, 324463681U, 385795061U, 551597588U, 21339008U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 280U, 127692781U, 389432875U, 357132832U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 42U, 537767569U, 410450016U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 6U, 356826688U], - [0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U] + 2631U, 149457141U, 577092685U, 632289089U, 81912456U, 221591423U, 502967496U, 403284731U, 377738089U, 492128779U, 746799U, 366351977U, 190199623U, 38066284U, 526403762U, 650603058U, 454901440U, + 0U, 402U, 68350375U, 30641941U, 266024478U, 208884256U, 571208415U, 337765723U, 215140626U, 129419325U, 480359048U, 398051646U, 635841659U, 214020719U, 136986618U, 626219915U, 49699360U, + 0U, 0U, 61U, 295059608U, 141201404U, 517024870U, 239296485U, 527697587U, 212906911U, 453637228U, 467589845U, 144614682U, 45134568U, 184514320U, 644355351U, 104784612U, 308625792U, + 0U, 0U, 0U, 9U, 256449755U, 500124311U, 479690581U, 372802935U, 413254725U, 487877412U, 520263169U, 176791855U, 78190744U, 291820402U, 74998585U, 496097732U, 59100544U, + 0U, 0U, 0U, 0U, 1U, 285573662U, 455976778U, 379818553U, 100001224U, 448949512U, 109507367U, 117185012U, 347328982U, 522665809U, 36908802U, 577276849U, 64504928U, + 0U, 0U, 0U, 0U, 0U, 0U, 143945778U, 651677945U, 281429047U, 535878743U, 264290972U, 526964023U, 199595821U, 597442702U, 499113091U, 424550935U, 458949280U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 21997789U, 294590275U, 148640294U, 595017589U, 210481832U, 404203788U, 574729546U, 160126051U, 430102516U, 44963712U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 3361701U, 325788598U, 30977630U, 513969330U, 194569730U, 164019635U, 136596846U, 626087230U, 503769920U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 513735U, 77223048U, 437087610U, 300156666U, 605448490U, 214625350U, 141436834U, 379377856U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 78508U, 646269101U, 118408823U, 91512303U, 209184527U, 413102373U, 153715680U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 11997U, 486083817U, 3737691U, 294005210U, 247894721U, 289024608U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1833U, 324463681U, 385795061U, 551597588U, 21339008U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 280U, 127692781U, 389432875U, 357132832U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 42U, 537767569U, 410450016U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 6U, 356826688U, + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U, ]; - // EncodeTable64 flattened to a contiguous row-major ulong[]: TensorMultiplyAdd vector-loads a - // whole source-limb row at once, so rows must be contiguous, and ulong matches the accumulator / - // vector element type (no per-element widening). Same values as the jagged table, via Flatten. - internal static readonly ulong[] EncodeTable64RowMajor = Flatten(EncodeTable64, IntermediateSz64 - 1); - - // Decode coefficients stored column-major (transposed): DecodeTable64[j * IntermediateSz64 + i] - // holds the coefficient for output limb j, intermediate limb i. Each output limb's column of - // IntermediateSz64 coefficients is contiguous, so it can be fed straight into TensorDot. - internal static readonly ulong[] DecodeTable64 = + // Element j * IntermediateSz64 + i is the coefficient of intermediate limb i in output limb j. + // Each output limb's coefficients are contiguous, so they feed straight into TensorDot. + internal static ReadOnlySpan DecodeTable64 => [ 249448U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 3719864065U, 1632305U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, @@ -104,18 +92,4 @@ internal static class Base58BitcoinTables 0U, 0U, 0U, 0U, 0U, 268435456U, 176160768U, 2485387264U, 1468637184U, 2959155456U, 1199103528U, 485140318U, 1483338760U, 2595180627U, 1933902296U, 100304420U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1073741824U, 4194304000U, 17825792U, 4063920128U, 3355157504U, 656356768U, 1U, ]; - - private static ulong[] Flatten(uint[][] rows, int rowLength) - { - var flat = new ulong[rows.Length * rowLength]; - for (int i = 0; i < rows.Length; i++) - { - for (int j = 0; j < rowLength; j++) - { - flat[i * rowLength + j] = rows[i][j]; - } - } - - return flat; - } }