diff --git a/src/Nethermind/Nethermind.Evm.Precompiles/BN254.cs b/src/Nethermind/Nethermind.Evm.Precompiles/BN254.cs index fb4ced5f60f1..b2b6e2a74bee 100644 --- a/src/Nethermind/Nethermind.Evm.Precompiles/BN254.cs +++ b/src/Nethermind/Nethermind.Evm.Precompiles/BN254.cs @@ -2,8 +2,12 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Buffers.Binary; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; using Nethermind.MclBindings; namespace Nethermind.Evm.Precompiles; @@ -21,50 +25,59 @@ static BN254() throw new InvalidOperationException("MCL initialization failed"); } - internal static bool Add(Span input, Span output) + [MethodImpl(MethodImplOptions.NoInlining)] + internal static bool Add(byte[] output, ReadOnlySpan input) { - if (input.Length != 128) - return false; + const int chunkSize = 64; - if (!DeserializeG1(input[0..64], out mclBnG1 x)) - return false; + Debug.Assert(input.Length == 128); + Debug.Assert(output.Length == 64); - if (!DeserializeG1(input[64..128], out mclBnG1 y)) - return false; + fixed (byte* data = &MemoryMarshal.GetReference(input)) + { + if (!DeserializeG1(data, out mclBnG1 x)) + return false; + + if (!DeserializeG1(data + chunkSize, out mclBnG1 y)) + return false; - mclBnG1_add(ref x, x, y); // x += y - mclBnG1_normalize(ref x, x); + mclBnG1_add(ref x, x, y); // x += y + mclBnG1_normalize(ref x, x); - return SerializeG1(x, output); + return SerializeG1(x, output); + } } - internal static bool Mul(Span input, Span output) + [MethodImpl(MethodImplOptions.NoInlining)] + internal static bool Mul(byte[] output, ReadOnlySpan input) { - if (input.Length != 96) - return false; - - if (!DeserializeG1(input[0..64], out mclBnG1 x)) - return false; + const int chunkSize = 64; - Span yData = input[64..]; - yData.Reverse(); // To little-endian + Debug.Assert(input.Length == 96); + Debug.Assert(output.Length == 64); - mclBnFr y = default; - - fixed (byte* ptr = &MemoryMarshal.GetReference(yData)) + fixed (byte* data = &MemoryMarshal.GetReference(input)) { - if (mclBnFr_setLittleEndianMod(ref y, (nint)ptr, 32) == -1 || mclBnFr_isValid(y) == 0) + if (!DeserializeG1(data, out mclBnG1 x)) return false; - } - mclBnG1_mul(ref x, x, y); // x *= y - mclBnG1_normalize(ref x, x); + Unsafe.SkipInit(out mclBnFr y); + if (mclBnFr_setBigEndianMod(ref y, (nint)data + chunkSize, 32) == -1 || mclBnFr_isValid(y) == 0) + return false; - return SerializeG1(x, output); + mclBnG1_mul(ref x, x, y); // x *= y + mclBnG1_normalize(ref x, x); + return SerializeG1(x, output); + } } - internal static bool CheckPairing(Span input, Span output) + [MethodImpl(MethodImplOptions.NoInlining)] + internal static bool CheckPairing(byte[] output, ReadOnlySpan input) { + if ((uint)output.Length < 32) + return false; + + // Empty input means "true" by convention if (input.Length == 0) { output[31] = 1; @@ -74,138 +87,307 @@ internal static bool CheckPairing(Span input, Span output) if (input.Length % PairSize != 0) return false; - mclBnGT gt = default; - Unsafe.SkipInit(out mclBnGT previous); - var hasPrevious = false; - - for (int i = 0, count = input.Length; i < count; i += PairSize) + fixed (byte* data = &MemoryMarshal.GetReference(input)) { - var i64 = i + 64; - - if (!DeserializeG1(input[i..i64], out mclBnG1 g1)) - return false; - - if (!DeserializeG2(input[i64..(i64 + 128)], out mclBnG2 g2)) - return false; - - if (mclBnG1_isZero(g1) == 1 || mclBnG2_isZero(g2) == 1) - continue; + Unsafe.SkipInit(out mclBnGT ml); + Unsafe.SkipInit(out mclBnGT acc); + bool hasMl = false; + + for (int i = 0; i < input.Length; i += PairSize) + { + if (!DeserializeG1(data + i, out mclBnG1 g1)) + return false; + + if (!DeserializeG2(data + i + 64, out mclBnG2 g2)) + return false; + + // Skip if g1 or g2 are zero + if (IsZero(g1) || IsZero(g2)) + continue; + + mclBn_millerLoop(ref hasMl ? ref ml : ref acc, g1, g2); // Miller loop only + + if (hasMl) + { + mclBnGT_mul(ref acc, acc, ml); + } + else + { + hasMl = true; + } + } + + // All pairs had zero element -> valid + if (!hasMl) + { + output[31] = 1; + return true; + } + + // Single final exponentiation for the product + mclBn_finalExp(ref acc, acc); + + // True if the product of pairings equals 1 in GT + output[31] = Convert.ToByte(mclBnGT_isOne(acc) == 1); + } + return true; + } - mclBn_pairing(ref gt, g1, g2); + private static bool IsZero(in T data) + where T : unmanaged, allows ref struct + { + ref byte start = ref Unsafe.As(ref Unsafe.AsRef(in data)); + ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpan(in start, sizeof(T)); + return span.IndexOfAnyExcept((byte)0) < 0; + } - // Skip multiplication for the first pairing as there's no previous result - if (hasPrevious) - mclBnGT_mul(ref gt, gt, previous); // gt *= previous + private static bool DeserializeG1(byte* data, out mclBnG1 point) + { + const int chunkSize = 32; - previous = gt; - hasPrevious = true; - } + point = default; - // If gt is zero, then no pairing was computed, and it's considered valid - if (mclBnGT_isOne(gt) == 1 || mclBnGT_isZero(gt) == 1) + // Treat all-zero as point at infinity for your calling convention + if (IsZero64(data)) { - output[31] = 1; return true; } - return mclBnGT_isValid(gt) == 1; + // Input is big-endian; MCL call below expects little-endian byte order for Fp + byte* tmp = stackalloc byte[chunkSize]; + + // x + CopyReverse32(data, tmp); + if (mclBnFp_deserialize(ref point.x, (nint)tmp, chunkSize) == nuint.Zero) + return false; + // y + CopyReverse32(data + chunkSize, tmp); + if (mclBnFp_deserialize(ref point.y, (nint)tmp, chunkSize) == nuint.Zero) + return false; + + mclBnFp_setInt32(ref point.z, 1); + return mclBnG1_isValid(point) == 1; } - private static bool DeserializeG1(Span data, out mclBnG1 point) + private static bool DeserializeG2(byte* data, out mclBnG2 point) { + const int chunkSize = 32; + point = default; - // Check for all-zero data - if (data.IndexOfAnyExcept((byte)0) == -1) + // Treat all-zero as point at infinity + if (IsZero128(data)) + { return true; + } - Span x = data[0..32]; - x.Reverse(); // To little-endian + // Input layout: x_im, x_re, y_im, y_re (each 32 bytes, big-endian) + // MCL Fp2 layout: d0 = re, d1 = im + byte* tmp = stackalloc byte[chunkSize]; - fixed (byte* ptr = &MemoryMarshal.GetReference(x)) - { - if (mclBnFp_deserialize(ref point.x, (nint)ptr, 32) == nuint.Zero) - return false; - } + // x.im + CopyReverse32(data, tmp); + if (mclBnFp_deserialize(ref point.x.d1, (nint)tmp, chunkSize) == nuint.Zero) + return false; - Span y = data[32..64]; - y.Reverse(); // To little-endian + // x.re + CopyReverse32(data + chunkSize, tmp); + if (mclBnFp_deserialize(ref point.x.d0, (nint)tmp, chunkSize) == nuint.Zero) + return false; - fixed (byte* ptr = &MemoryMarshal.GetReference(y)) - { - if (mclBnFp_deserialize(ref point.y, (nint)ptr, 32) == nuint.Zero) - return false; - } + // y.im + CopyReverse32(data + chunkSize * 2, tmp); + if (mclBnFp_deserialize(ref point.y.d1, (nint)tmp, chunkSize) == nuint.Zero) + return false; - mclBnFp_setInt32(ref point.z, 1); + // y.re + CopyReverse32(data + chunkSize * 3, tmp); + if (mclBnFp_deserialize(ref point.y.d0, (nint)tmp, chunkSize) == nuint.Zero) + return false; - return mclBnG1_isValid(point) == 1; + mclBnFp_setInt32(ref point.z.d0, 1); + + return mclBnG2_isValid(point) == 1 && mclBnG2_isValidOrder(point) == 1; } - private static bool DeserializeG2(Span data, out mclBnG2 point) + private static bool SerializeG1(in mclBnG1 point, byte[] output) { - point = default; - - // Check for all-zero data - if (data.IndexOfAnyExcept((byte)0) == -1) - return true; + const int chunkSize = 32; - Span x0 = data[32..64]; - Span x1 = data[0..32]; - x0.Reverse(); // To little-endian - x1.Reverse(); // To little-endian - - fixed (byte* ptr0 = &MemoryMarshal.GetReference(x0)) - fixed (byte* ptr1 = &MemoryMarshal.GetReference(x1)) + fixed (byte* ptr = &MemoryMarshal.GetArrayDataReference(output)) { - if (mclBnFp_deserialize(ref point.x.d0, (nint)ptr0, 32) == nuint.Zero) + if (mclBnFp_getLittleEndian((nint)ptr, chunkSize, point.x) == nuint.Zero) return false; - if (mclBnFp_deserialize(ref point.x.d1, (nint)ptr1, 32) == nuint.Zero) + if (mclBnFp_getLittleEndian((nint)ptr + chunkSize, chunkSize, point.y) == nuint.Zero) return false; + + CopyReverse32(ptr, ptr); // To big-endian + CopyReverse32(ptr + chunkSize, ptr + chunkSize); // To big-endian } - Span y0 = data[96..128]; - Span y1 = data[64..96]; - y0.Reverse(); // To little-endian - y1.Reverse(); // To little-endian + return true; + } + + private static unsafe bool IsZero64(byte* ptr) + { + const int Length = 64; - fixed (byte* ptr0 = &MemoryMarshal.GetReference(y0)) - fixed (byte* ptr1 = &MemoryMarshal.GetReference(y1)) + if (Vector512.IsHardwareAccelerated) { - if (mclBnFp_deserialize(ref point.y.d0, (nint)ptr0, 32) == nuint.Zero) - return false; - - if (mclBnFp_deserialize(ref point.y.d1, (nint)ptr1, 32) == nuint.Zero) - return false; + Vector512 a = Unsafe.ReadUnaligned>(ptr); + return a == default; + } + else if (Vector256.IsHardwareAccelerated) + { + Vector256 a = Unsafe.ReadUnaligned>(ptr); + Vector256 b = Unsafe.ReadUnaligned>(ptr + Vector256.Count); + Vector256 o = Vector256.BitwiseOr(a, b); + return o == default; + } + else if (Vector128.IsHardwareAccelerated) + { + // 4x16-byte blocks, coalesced in pairs + for (nuint offset = 0; offset < Length; offset += (nuint)Vector128.Count * 2) + { + Vector128 a = Unsafe.ReadUnaligned>(ptr + offset); + Vector128 b = Unsafe.ReadUnaligned>(ptr + offset + Vector128.Count); + Vector128 o = Vector128.BitwiseOr(a, b); + if (o != default) return false; + } + return true; + } + else + { + // scalar fallback + ulong* x = (ulong*)ptr; + for (int i = 0; i < 8; i++) + { + if (x[i] != 0) + return false; + } + return true; } + } - mclBnFp_setInt32(ref point.z.d0, 1); + private static unsafe bool IsZero128(byte* ptr) + { + const int Length = 128; - return mclBnG2_isValid(point) == 1 && mclBnG2_isValidOrder(point) == 1; + if (Vector512.IsHardwareAccelerated) + { + // 2x512 -> OR‑reduce -> EqualsAll + Vector512 a = Unsafe.ReadUnaligned>(ptr + 0); + Vector512 b = Unsafe.ReadUnaligned>(ptr + Vector512.Count); + Vector512 o = Vector512.BitwiseOr(a, b); + return o == default; + } + else if (Vector256.IsHardwareAccelerated) + { + // 4x32-byte blocks, coalesced in pairs (2 loads per iteration) + for (nuint offset = 0; offset < Length; offset += (nuint)Vector256.Count * 2) + { + Vector256 a = Unsafe.ReadUnaligned>(ptr + offset); + Vector256 b = Unsafe.ReadUnaligned>(ptr + offset + Vector256.Count); + Vector256 o = Vector256.BitwiseOr(a, b); + if (o != default) return false; + } + return true; + } + else if (Vector128.IsHardwareAccelerated) + { + // 8x16-byte blocks, coalesced in pairs + for (nuint offset = 0; offset < Length; offset += (nuint)Vector128.Count * 2) + { + Vector128 a = Unsafe.ReadUnaligned>(ptr + offset); + Vector128 b = Unsafe.ReadUnaligned>(ptr + offset + Vector128.Count); + Vector128 o = Vector128.BitwiseOr(a, b); + if (o != default) return false; + } + return true; + } + else + { + // scalar fallback + ulong* x = (ulong*)ptr; + for (int i = 0; i < 16; i++) + { + if (x[i] != 0) + return false; + } + return true; + } } - private static bool SerializeG1(in mclBnG1 point, Span output) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CopyReverse32(byte* srcRef, byte* dstRef) { - Span x = output[0..32]; - - fixed (byte* ptr = &MemoryMarshal.GetReference(x)) + if (Avx2.IsSupported) { - if (mclBnFp_getLittleEndian((nint)ptr, 32, point.x) == nuint.Zero) - return false; + Reverse32BytesAvx2(srcRef, dstRef); + } + else if (Vector128.IsHardwareAccelerated) + { + Reverse32Bytes128(srcRef, dstRef); + } + else + { + // Fallback scalar path + Reverse32BytesScalar(srcRef, dstRef); } + } - Span y = output[32..64]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Reverse32BytesAvx2(byte* srcRef, byte* dstRef) + { + // Load 32 bytes as one 256-bit vector + Vector256 vec = Unsafe.ReadUnaligned>(srcRef); + Vector256 fullRev; - fixed (byte* ptr = &MemoryMarshal.GetReference(y)) + Vector256 mask = Vector256.Create((byte)31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + if (Avx512Vbmi.VL.IsSupported) { - if (mclBnFp_getLittleEndian((nint)ptr, 32, point.y) == nuint.Zero) - return false; + fullRev = Avx512Vbmi.VL.PermuteVar32x8(vec, mask); + } + else + { + Vector256 revInLane = Avx2.Shuffle(vec, mask); + fullRev = Avx2.Permute2x128(revInLane, revInLane, 0x01); } - x.Reverse(); // To big-endian - y.Reverse(); // To big-endian + Unsafe.WriteUnaligned(dstRef, fullRev); + } - return true; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Reverse32Bytes128(byte* srcRef, byte* dstRef) + { + // Two 16-byte halves: reverse each then swap them + Vector128 lo = Unsafe.ReadUnaligned>(srcRef); + Vector128 hi = Unsafe.ReadUnaligned>(srcRef + Vector128.Count); + + Vector128 indices = Vector128.Create((byte)15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); + lo = Vector128.Shuffle(lo, indices); + hi = Vector128.Shuffle(hi, indices); + + // Store swapped halves reversed + Unsafe.WriteUnaligned(dstRef, hi); + Unsafe.WriteUnaligned(dstRef + Vector128.Count, lo); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Reverse32BytesScalar(byte* srcRef, byte* dstRef) + { + ulong* src = (ulong*)srcRef; + ulong* dst = (ulong*)dstRef; + + ulong a = BinaryPrimitives.ReverseEndianness(src[0]); + ulong b = BinaryPrimitives.ReverseEndianness(src[1]); + ulong c = BinaryPrimitives.ReverseEndianness(src[2]); + ulong d = BinaryPrimitives.ReverseEndianness(src[3]); + + dst[0] = d; + dst[1] = c; + dst[2] = b; + dst[3] = a; } } diff --git a/src/Nethermind/Nethermind.Evm.Precompiles/BN254AddPrecompile.cs b/src/Nethermind/Nethermind.Evm.Precompiles/BN254AddPrecompile.cs index bc46669b04a8..a35d4e0d2e4e 100644 --- a/src/Nethermind/Nethermind.Evm.Precompiles/BN254AddPrecompile.cs +++ b/src/Nethermind/Nethermind.Evm.Precompiles/BN254AddPrecompile.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Runtime.CompilerServices; using Nethermind.Core; using Nethermind.Core.Specs; @@ -10,6 +11,9 @@ namespace Nethermind.Evm.Precompiles; /// public class BN254AddPrecompile : IPrecompile { + private const int InputLength = 128; + private const int OutputLength = 64; + public static readonly BN254AddPrecompile Instance = new(); public static Address Address { get; } = Address.FromNumber(6); @@ -22,15 +26,33 @@ public class BN254AddPrecompile : IPrecompile public long DataGasCost(ReadOnlyMemory inputData, IReleaseSpec releaseSpec) => 0L; + [SkipLocalsInit] public (byte[], bool) Run(ReadOnlyMemory inputData, IReleaseSpec releaseSpec) { Metrics.Bn254AddPrecompile++; - Span input = stackalloc byte[128]; - Span output = stackalloc byte[64]; + ReadOnlySpan input = inputData.Span; + if (InputLength < (uint)input.Length) + { + // Input is too long - trim to the expected length. + input = input[..InputLength]; + } + + byte[] output = new byte[OutputLength]; + bool result = (input.Length == InputLength) ? + BN254.Add(output, input) : + RunPaddedInput(output, input); - inputData.Span[0..Math.Min(inputData.Length, input.Length)].CopyTo(input); + return result ? (output, true) : IPrecompile.Failure; + } - return BN254.Add(input, output) ? (output.ToArray(), true) : IPrecompile.Failure; + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool RunPaddedInput(byte[] output, ReadOnlySpan input) + { + // Input is too short - pad with zeros up to the expected length. + Span padded = stackalloc byte[InputLength]; + // Copies input bytes; rest of the span is already zero-initialized. + input.CopyTo(padded); + return BN254.Add(output, padded); } } diff --git a/src/Nethermind/Nethermind.Evm.Precompiles/BN254MulPrecompile.cs b/src/Nethermind/Nethermind.Evm.Precompiles/BN254MulPrecompile.cs index e5b2796bc83a..c41258b6368f 100644 --- a/src/Nethermind/Nethermind.Evm.Precompiles/BN254MulPrecompile.cs +++ b/src/Nethermind/Nethermind.Evm.Precompiles/BN254MulPrecompile.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Runtime.CompilerServices; using Nethermind.Core; using Nethermind.Core.Specs; @@ -10,6 +11,9 @@ namespace Nethermind.Evm.Precompiles; /// public class BN254MulPrecompile : IPrecompile { + private const int InputLength = 96; + private const int OutputLength = 64; + public static readonly BN254MulPrecompile Instance = new(); public static Address Address { get; } = Address.FromNumber(7); @@ -22,15 +26,34 @@ public class BN254MulPrecompile : IPrecompile public long DataGasCost(ReadOnlyMemory inputData, IReleaseSpec releaseSpec) => 0L; + [SkipLocalsInit] public (byte[], bool) Run(ReadOnlyMemory inputData, IReleaseSpec releaseSpec) { Metrics.Bn254MulPrecompile++; - Span input = stackalloc byte[96]; - Span output = stackalloc byte[64]; + ReadOnlySpan input = inputData.Span; + if (InputLength < (uint)input.Length) + { + // Input is too long - trim to the expected length. + input = input[..InputLength]; + } + + byte[] output = new byte[OutputLength]; + bool result = (input.Length == InputLength) ? + BN254.Mul(output, input) : + RunPaddedInput(output, input); - inputData.Span[0..Math.Min(inputData.Length, input.Length)].CopyTo(input); + return result ? (output, true) : IPrecompile.Failure; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool RunPaddedInput(byte[] output, ReadOnlySpan input) + { + // Input is too short - pad with zeros up to the expected length. + Span padded = stackalloc byte[InputLength]; + // Copies input bytes; rest of the span is already zero-initialized. + input.CopyTo(padded); - return BN254.Mul(input, output) ? (output.ToArray(), true) : IPrecompile.Failure; + return BN254.Mul(output, padded); } } diff --git a/src/Nethermind/Nethermind.Evm.Precompiles/BN254PairingPrecompile.cs b/src/Nethermind/Nethermind.Evm.Precompiles/BN254PairingPrecompile.cs index ff0b8292062d..855e65126606 100644 --- a/src/Nethermind/Nethermind.Evm.Precompiles/BN254PairingPrecompile.cs +++ b/src/Nethermind/Nethermind.Evm.Precompiles/BN254PairingPrecompile.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; -using System.Buffers; using Nethermind.Core; using Nethermind.Core.Specs; @@ -36,15 +35,9 @@ public long DataGasCost(ReadOnlyMemory inputData, IReleaseSpec releaseSpec return IPrecompile.Failure; } - var input = ArrayPool.Shared.Rent(inputData.Length); - Span output = stackalloc byte[32]; + byte[] output = new byte[32]; + bool result = BN254.CheckPairing(output, inputData.Span); - inputData.CopyTo(input); - - var result = BN254.CheckPairing(input.AsSpan(0, inputData.Length), output); - - ArrayPool.Shared.Return(input); - - return result ? (output.ToArray(), true) : IPrecompile.Failure; + return result ? (output, true) : IPrecompile.Failure; } } diff --git a/src/Nethermind/Nethermind.Precompiles.Benchmark/PrecompileBenchmarkBase.cs b/src/Nethermind/Nethermind.Precompiles.Benchmark/PrecompileBenchmarkBase.cs index 132d06e8228c..b3d99b3283aa 100644 --- a/src/Nethermind/Nethermind.Precompiles.Benchmark/PrecompileBenchmarkBase.cs +++ b/src/Nethermind/Nethermind.Precompiles.Benchmark/PrecompileBenchmarkBase.cs @@ -16,6 +16,8 @@ namespace Nethermind.Precompiles.Benchmark { public abstract class PrecompileBenchmarkBase { + private const int Operations = 25; + protected abstract IEnumerable Precompiles { get; } protected abstract string InputsDirectory { get; } @@ -50,14 +52,14 @@ public IEnumerable Inputs // take only first line from each file inputs.AddRange(File.ReadAllLines(file) .Select(LineToTestInput).Take(1).ToArray() - .Select(i => new Param(precompile, file, i, null))); + .Select(i => new Param(precompile, Path.GetFileName(file), i, null))); } foreach (string file in Directory.GetFiles(inputsDir, "*.json", SearchOption.TopDirectoryOnly)) { EthereumJsonSerializer jsonSerializer = new(); JsonInput[] jsonInputs = jsonSerializer.Deserialize(File.ReadAllText(file)); - IEnumerable parameters = jsonInputs.Select(i => new Param(precompile, i.Name!, i.Input!, i.Expected)); + IEnumerable parameters = jsonInputs.Select(i => new Param(precompile, Path.GetFileName(i.Name!), i.Input!, i.Expected)); inputs.AddRange(parameters); } @@ -75,8 +77,16 @@ public IEnumerable Inputs private static byte[] LineToTestInput(string line) => Bytes.FromHexString(line.Split(',')[0]); - [Benchmark(Baseline = true)] + [Benchmark(Baseline = true, OperationsPerInvoke = Operations)] public (ReadOnlyMemory, bool) Baseline() - => Input.Precompile.Run(Input.Bytes, Cancun.Instance); + { + bool overallResult = true; + for (var i = 0; i < Operations; i++) + { + (_, bool result) = Input.Precompile.Run(Input.Bytes, Cancun.Instance); + overallResult &= result; + } + return (default, overallResult); + } } }