From 319d234582ed2e3193eed17631eba26cc1226cc4 Mon Sep 17 00:00:00 2001 From: benaadams Date: Thu, 11 Jan 2024 17:49:42 +0000 Subject: [PATCH] Optimize Jump Destination Analysis --- .../CodeDataAnalyzerHelperTests.cs | 45 ----- .../CodeAnalysis/CodeInfoTests.cs | 53 +----- .../CodeAnalysis/CodeDataAnalyzer.cs | 170 ------------------ .../Nethermind.Evm/CodeAnalysis/CodeInfo.cs | 46 +---- .../CodeAnalysis/ICodeInfoAnalyzer.cs | 10 -- .../CodeAnalysis/JumpDestinationAnalyzer.cs | 112 ++++++++++++ .../CodeAnalysis/JumpdestAnalyzer.cs | 70 -------- 7 files changed, 129 insertions(+), 377 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeDataAnalyzerHelperTests.cs delete mode 100644 src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeDataAnalyzer.cs delete mode 100644 src/Nethermind/Nethermind.Evm/CodeAnalysis/ICodeInfoAnalyzer.cs create mode 100644 src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpDestinationAnalyzer.cs delete mode 100644 src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpdestAnalyzer.cs diff --git a/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeDataAnalyzerHelperTests.cs b/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeDataAnalyzerHelperTests.cs deleted file mode 100644 index 729e432b1cf..00000000000 --- a/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeDataAnalyzerHelperTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using FluentAssertions; -using Nethermind.Evm.CodeAnalysis; -using NUnit.Framework; - -namespace Nethermind.Evm.Test.CodeAnalysis -{ - [TestFixture] - public class CodeDataAnalyzerHelperTests - { - [Test] - public void Validate_CodeBitmap_With_Push10() - { - byte[] code = - { - (byte)Instruction.PUSH10, - 1,2,3,4,5,6,7,8,9,10, - (byte)Instruction.JUMPDEST - }; - - var bitmap = CodeDataAnalyzerHelper.CreateCodeBitmap(code); - bitmap[0].Should().Be(127); - bitmap[1].Should().Be(224); - } - - [Test] - public void Validate_CodeBitmap_With_Push30() - { - byte[] code = - { - (byte)Instruction.PUSH30, - 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30, - (byte)Instruction.JUMPDEST - }; - - var bitmap = CodeDataAnalyzerHelper.CreateCodeBitmap(code); - bitmap[0].Should().Be(127); - bitmap[1].Should().Be(255); - bitmap[2].Should().Be(255); - bitmap[3].Should().Be(254); - } - } -} diff --git a/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeInfoTests.cs b/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeInfoTests.cs index c2cf0817de1..cf2c6e68472 100644 --- a/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeInfoTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/CodeAnalysis/CodeInfoTests.cs @@ -106,7 +106,7 @@ public void Validate_CodeBitmap_With_Push30() } [Test] - public void Small_Jumpdest_Use_CodeDataAnalyzer() + public void Small_Jumpdest() { byte[] code = { @@ -116,15 +116,10 @@ public void Small_Jumpdest_Use_CodeDataAnalyzer() CodeInfo codeInfo = new(code); codeInfo.ValidateJump(10, false).Should().BeTrue(); - - FieldInfo field = typeof(CodeInfo).GetField(AnalyzerField, BindingFlags.Instance | BindingFlags.NonPublic); - var calc = field.GetValue(codeInfo); - - Assert.IsInstanceOf(calc); } [Test] - public void Small_Push1_Use_CodeDataAnalyzer() + public void Small_Push1() { byte[] code = { @@ -134,45 +129,30 @@ public void Small_Push1_Use_CodeDataAnalyzer() CodeInfo codeInfo = new(code); codeInfo.ValidateJump(10, false).Should().BeFalse(); - - FieldInfo field = typeof(CodeInfo).GetField(AnalyzerField, BindingFlags.Instance | BindingFlags.NonPublic); - var calc = field.GetValue(codeInfo); - - Assert.IsInstanceOf(calc); } [Test] - public void Jumpdest_Over10k_Use_JumpdestAnalyzer() + public void Jumpdest_Over10k() { var code = Enumerable.Repeat((byte)0x5b, 10_001).ToArray(); CodeInfo codeInfo = new(code); codeInfo.ValidateJump(10, false).Should().BeTrue(); - - FieldInfo field = typeof(CodeInfo).GetField(AnalyzerField, BindingFlags.Instance | BindingFlags.NonPublic); - var calc = field.GetValue(codeInfo); - - Assert.IsInstanceOf(calc); } [Test] - public void Push1_Over10k_Use_JumpdestAnalyzer() + public void Push1_Over10k() { var code = Enumerable.Repeat((byte)0x60, 10_001).ToArray(); CodeInfo codeInfo = new(code); codeInfo.ValidateJump(10, false).Should().BeFalse(); - - FieldInfo field = typeof(CodeInfo).GetField(AnalyzerField, BindingFlags.Instance | BindingFlags.NonPublic); - var calc = field.GetValue(codeInfo); - - Assert.IsInstanceOf(calc); } [Test] - public void Push1Jumpdest_Over10k_Use_JumpdestAnalyzer() + public void Push1Jumpdest_Over10k() { byte[] code = new byte[10_001]; for (int i = 0; i < code.Length; i++) @@ -180,27 +160,10 @@ public void Push1Jumpdest_Over10k_Use_JumpdestAnalyzer() code[i] = i % 2 == 0 ? (byte)0x60 : (byte)0x5b; } - ICodeInfoAnalyzer calc = null; - int iterations = 1; - while (iterations <= 10) - { - CodeInfo codeInfo = new(code); - - codeInfo.ValidateJump(10, false).Should().BeFalse(); - codeInfo.ValidateJump(11, false).Should().BeFalse(); // 0x5b but not JUMPDEST but data - - FieldInfo field = typeof(CodeInfo).GetField(AnalyzerField, BindingFlags.Instance | BindingFlags.NonPublic); - calc = (ICodeInfoAnalyzer)field.GetValue(codeInfo); - - if (calc is JumpdestAnalyzer) - { - break; - } - - iterations++; - } + CodeInfo codeInfo = new(code); - Assert.IsInstanceOf(calc); + codeInfo.ValidateJump(10, false).Should().BeFalse(); + codeInfo.ValidateJump(11, false).Should().BeFalse(); // 0x5b but not JUMPDEST but data } } } diff --git a/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeDataAnalyzer.cs b/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeDataAnalyzer.cs deleted file mode 100644 index a3dd20dcb53..00000000000 --- a/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeDataAnalyzer.cs +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System; - -namespace Nethermind.Evm.CodeAnalysis -{ - public class CodeDataAnalyzer : ICodeInfoAnalyzer - { - private byte[]? _codeBitmap; - public byte[] MachineCode { get; set; } - - public CodeDataAnalyzer(byte[] code) - { - MachineCode = code; - } - - public bool ValidateJump(int destination, bool isSubroutine) - { - _codeBitmap ??= CodeDataAnalyzerHelper.CreateCodeBitmap(MachineCode); - - if (destination < 0 || destination >= MachineCode.Length) - { - return false; - } - - if (!CodeDataAnalyzerHelper.IsCodeSegment(_codeBitmap, destination)) - { - return false; - } - - if (isSubroutine) - { - return MachineCode[destination] == 0x5c; - } - - return MachineCode[destination] == 0x5b; - } - } - - public static class CodeDataAnalyzerHelper - { - private const UInt16 Set2BitsMask = 0b1100_0000_0000_0000; - private const UInt16 Set3BitsMask = 0b1110_0000_0000_0000; - private const UInt16 Set4BitsMask = 0b1111_0000_0000_0000; - private const UInt16 Set5BitsMask = 0b1111_1000_0000_0000; - private const UInt16 Set6BitsMask = 0b1111_1100_0000_0000; - private const UInt16 Set7BitsMask = 0b1111_1110_0000_0000; - - private static readonly byte[] _lookup = new byte[8] { 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1, }; - - /// - /// Collects data locations in code. - /// An unset bit means the byte is an opcode, a set bit means it's data. - /// - public static byte[] CreateCodeBitmap(byte[] code) - { - // The bitmap is 4 bytes longer than necessary, in case the code - // ends with a PUSH32, the algorithm will push zeroes onto the - // bitvector outside the bounds of the actual code. - byte[] bitvec = new byte[(code.Length / 8) + 1 + 4]; - - byte push1 = 0x60; - byte push32 = 0x7f; - - for (int pc = 0; pc < code.Length;) - { - byte op = code[pc]; - pc++; - - if (op < push1 || op > push32) - { - continue; - } - - int numbits = op - push1 + 1; - - if (numbits >= 8) - { - for (; numbits >= 16; numbits -= 16) - { - bitvec.Set16(pc); - pc += 16; - } - - for (; numbits >= 8; numbits -= 8) - { - bitvec.Set8(pc); - pc += 8; - } - } - - switch (numbits) - { - case 1: - bitvec.Set1(pc); - pc += 1; - break; - case 2: - bitvec.SetN(pc, Set2BitsMask); - pc += 2; - break; - case 3: - bitvec.SetN(pc, Set3BitsMask); - pc += 3; - break; - case 4: - bitvec.SetN(pc, Set4BitsMask); - pc += 4; - break; - case 5: - bitvec.SetN(pc, Set5BitsMask); - pc += 5; - break; - case 6: - bitvec.SetN(pc, Set6BitsMask); - pc += 6; - break; - case 7: - bitvec.SetN(pc, Set7BitsMask); - pc += 7; - break; - } - } - - return bitvec; - } - - /// - /// Checks if the position is in a code segment. - /// - public static bool IsCodeSegment(byte[] bitvec, int pos) - { - return (bitvec[pos / 8] & (0x80 >> (pos % 8))) == 0; - } - - private static void Set1(this byte[] bitvec, int pos) - { - bitvec[pos / 8] |= _lookup[pos % 8]; - } - - private static void SetN(this byte[] bitvec, int pos, UInt16 flag) - { - ushort a = (ushort)(flag >> (pos % 8)); - bitvec[pos / 8] |= (byte)(a >> 8); - byte b = (byte)a; - if (b != 0) - { - // If the bit-setting affects the neighbouring byte, we can assign - no need to OR it, - // since it's the first write to that byte - bitvec[pos / 8 + 1] = b; - } - } - - private static void Set8(this byte[] bitvec, int pos) - { - byte a = (byte)(0xFF >> (pos % 8)); - bitvec[pos / 8] |= a; - bitvec[pos / 8 + 1] = (byte)~a; - } - - private static void Set16(this byte[] bitvec, int pos) - { - byte a = (byte)(0xFF >> (pos % 8)); - bitvec[pos / 8] |= a; - bitvec[pos / 8 + 1] = 0xFF; - bitvec[pos / 8 + 2] = (byte)~a; - } - } -} diff --git a/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeInfo.cs b/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeInfo.cs index ec8df2a5870..73d517ad78d 100644 --- a/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeInfo.cs +++ b/src/Nethermind/Nethermind.Evm/CodeAnalysis/CodeInfo.cs @@ -2,20 +2,17 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Runtime.CompilerServices; + using Nethermind.Evm.Precompiles; namespace Nethermind.Evm.CodeAnalysis { public class CodeInfo { - private const int SampledCodeLength = 10_001; - private const int PercentageOfPush1 = 40; - private const int NumberOfSamples = 100; - private static readonly Random _rand = new(); - public byte[] MachineCode { get; set; } public IPrecompile? Precompile { get; set; } - private ICodeInfoAnalyzer? _analyzer; + private JumpDestinationAnalyzer? _analyzer; public CodeInfo(byte[] code) { @@ -32,45 +29,20 @@ public CodeInfo(IPrecompile precompile) public bool ValidateJump(int destination, bool isSubroutine) { - if (_analyzer is null) - { - CreateAnalyzer(); - } + JumpDestinationAnalyzer analyzer = _analyzer; + analyzer ??= CreateAnalyzer(); - return _analyzer.ValidateJump(destination, isSubroutine); + return analyzer.ValidateJump(destination, isSubroutine); } /// /// Do sampling to choose an algo when the code is big enough. /// When the code size is small we can use the default analyzer. /// - private void CreateAnalyzer() + [MethodImpl(MethodImplOptions.NoInlining)] + private JumpDestinationAnalyzer CreateAnalyzer() { - if (MachineCode.Length >= SampledCodeLength) - { - byte push1Count = 0; - - // we check (by sampling randomly) how many PUSH1 instructions are in the code - for (int i = 0; i < NumberOfSamples; i++) - { - byte instruction = MachineCode[_rand.Next(0, MachineCode.Length)]; - - // PUSH1 - if (instruction == 0x60) - { - push1Count++; - } - } - - // If there are many PUSH1 ops then use the JUMPDEST analyzer. - // The JumpdestAnalyzer can perform up to 40% better than the default Code Data Analyzer - // in a scenario when the code consists only of PUSH1 instructions. - _analyzer = push1Count > PercentageOfPush1 ? new JumpdestAnalyzer(MachineCode) : new CodeDataAnalyzer(MachineCode); - } - else - { - _analyzer = new CodeDataAnalyzer(MachineCode); - } + return _analyzer = new JumpDestinationAnalyzer(MachineCode); } } } diff --git a/src/Nethermind/Nethermind.Evm/CodeAnalysis/ICodeInfoAnalyzer.cs b/src/Nethermind/Nethermind.Evm/CodeAnalysis/ICodeInfoAnalyzer.cs deleted file mode 100644 index 72b2fab0c17..00000000000 --- a/src/Nethermind/Nethermind.Evm/CodeAnalysis/ICodeInfoAnalyzer.cs +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -namespace Nethermind.Evm.CodeAnalysis -{ - public interface ICodeInfoAnalyzer - { - bool ValidateJump(int destination, bool isSubroutine); - } -} diff --git a/src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpDestinationAnalyzer.cs b/src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpDestinationAnalyzer.cs new file mode 100644 index 00000000000..9bcfdec311d --- /dev/null +++ b/src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpDestinationAnalyzer.cs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Nethermind.Evm.CodeAnalysis +{ + public sealed class JumpDestinationAnalyzer + { + private const int PUSH1 = 0x60; + private const int PUSH32 = 0x7f; + private const int JUMPDEST = 0x5b; + private const int BEGINSUB = 0x5c; + + private byte[]? _codeBitmap; + public byte[] MachineCode { get; set; } + + public JumpDestinationAnalyzer(byte[] code) + { + MachineCode = code; + } + + public bool ValidateJump(int destination, bool isSubroutine) + { + // Take array ref to local so Jit knows its size won't change in the method. + byte[] machineCode = MachineCode; + _codeBitmap ??= CreateJumpDestinationBitmap(machineCode); + + var result = false; + // Cast to uint to change negative numbers to very high numbers + // Then do length check, this both reduces check by 1 and eliminates the bounds + // check from accessing the array. + if ((uint)destination < (uint)machineCode.Length && + IsJumpDestination(_codeBitmap, destination)) + { + // Store byte to int, as less expensive operations at word size + int codeByte = machineCode[destination]; + if (isSubroutine) + { + result = codeByte == BEGINSUB; + } + else + { + result = codeByte == JUMPDEST; + } + } + + return result; + } + + /// + /// Collects data locations in code. + /// An unset bit means the byte is an opcode, a set bit means it's data. + /// + private static byte[] CreateJumpDestinationBitmap(byte[] code) + { + byte[] bitvec = new byte[(code.Length / 8) + 1]; + + int pc = 0; + while (true) + { + // Since we are using a non-standard for loop here + // Changing to while(true) plus below if check elides + // the bounds check from the array access + if ((uint)pc >= (uint)code.Length) break; + int instruction = code[pc]; + + if (instruction >= PUSH1 && instruction <= PUSH32) + { + pc += instruction - PUSH1 + 2; + } + else if (instruction == JUMPDEST || instruction == BEGINSUB) + { + Set(bitvec, pc); + pc++; + } + else + { + pc++; + } + } + + return bitvec; + } + + /// + /// Checks if the position is in a code segment. + /// + private static bool IsJumpDestination(byte[] bitvec, int pos) + { + //return (bitvec[pos / 8] & (0x80 >> (pos % 8))) == 0; + + int vecIndex = pos >> 3; + // Check if in bounds, Jit will add slightly more expensive exception throwing check if we don't + if ((uint)vecIndex >= (uint)bitvec.Length) return false; + + // Store byte to int, as less expensive operations at word size + int codeByte = bitvec[vecIndex]; + return (codeByte & (0x80 >> (pos & 7))) != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Set(byte[] bitvec, int pos) + { + int vecIndex = pos >> 3; + Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(bitvec), vecIndex) + |= (byte)(1 << (7 - (pos & 7))); + } + } +} diff --git a/src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpdestAnalyzer.cs b/src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpdestAnalyzer.cs deleted file mode 100644 index 5753199dd4b..00000000000 --- a/src/Nethermind/Nethermind.Evm/CodeAnalysis/JumpdestAnalyzer.cs +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using System.Collections; - -namespace Nethermind.Evm.CodeAnalysis -{ - public class JumpdestAnalyzer : ICodeInfoAnalyzer - { - private byte[] MachineCode { get; set; } - - private BitArray? _validJumpDestinations; - private BitArray? _validJumpSubDestinations; - - public JumpdestAnalyzer(byte[] code) - { - MachineCode = code; - } - - public bool ValidateJump(int destination, bool isSubroutine) - { - if (_validJumpDestinations is null) - { - CalculateJumpDestinations(); - } - - if (destination < 0 || destination >= _validJumpDestinations.Length || - (isSubroutine ? !_validJumpSubDestinations.Get(destination) : !_validJumpDestinations.Get(destination))) - { - return false; - } - - return true; - } - - private void CalculateJumpDestinations() - { - _validJumpDestinations = new BitArray(MachineCode.Length); - _validJumpSubDestinations = new BitArray(MachineCode.Length); - - int index = 0; - while (index < MachineCode.Length) - { - byte instruction = MachineCode[index]; - - // JUMPDEST - if (instruction == 0x5b) - { - _validJumpDestinations.Set(index, true); - } - // BEGINSUB - else if (instruction == 0x5c) - { - _validJumpSubDestinations.Set(index, true); - } - - // instruction >= Instruction.PUSH1 && instruction <= Instruction.PUSH32 - if (instruction >= 0x60 && instruction <= 0x7f) - { - //index += instruction - Instruction.PUSH1 + 2; - index += instruction - 0x60 + 2; - } - else - { - index++; - } - } - } - } -}