From f62d48649fa84ee890a9ac4b2ed6fd6773befae0 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Wed, 10 Jul 2024 09:01:53 +0200 Subject: [PATCH 1/8] core/vm: reuse `Memory` instances --- core/vm/interpreter.go | 1 + core/vm/memory.go | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 2b1ea3848352..6e7d28a0ce25 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -198,6 +198,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // they are returned to the pools defer func() { returnStack(stack) + mem.Free() }() contract.Input = input diff --git a/core/vm/memory.go b/core/vm/memory.go index e0202fd7c020..e8263b1f16e5 100644 --- a/core/vm/memory.go +++ b/core/vm/memory.go @@ -17,9 +17,19 @@ package vm import ( + "sync" + "github.com/holiman/uint256" ) +var memoryPool = sync.Pool{ + New: func() any { + return &Memory{ + store: make([]byte, 0), + } + }, +} + // Memory implements a simple memory model for the ethereum virtual machine. type Memory struct { store []byte @@ -28,7 +38,18 @@ type Memory struct { // NewMemory returns a new memory model. func NewMemory() *Memory { - return &Memory{} + return memoryPool.Get().(*Memory) +} + +// Free returns the memory to the pool. +func (m *Memory) Free() { + // To reduce peak allocation, return only smaller memory instances to the pool. + const maxBufferSize = 16 << 10 + if cap(m.store) <= maxBufferSize { + m.store = m.store[:0] + m.lastGasCost = 0 + memoryPool.Put(m) + } } // Set sets offset + size to value From b21288d229e56f3c3bb61b8f92856072b6ae9216 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Mon, 15 Jul 2024 13:42:21 +0200 Subject: [PATCH 2/8] make `memoryPool.New` logic identical to old `NewMemory` logic --- core/vm/memory.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/vm/memory.go b/core/vm/memory.go index e8263b1f16e5..7a03c9510d79 100644 --- a/core/vm/memory.go +++ b/core/vm/memory.go @@ -24,9 +24,7 @@ import ( var memoryPool = sync.Pool{ New: func() any { - return &Memory{ - store: make([]byte, 0), - } + return &Memory{} }, } From c59e9e5e6b28240093583a9a9f16bc060c8b1f7c Mon Sep 17 00:00:00 2001 From: lmittmann Date: Mon, 15 Jul 2024 16:32:52 +0200 Subject: [PATCH 3/8] clone memory in `ScopeContext.MemoryData` --- core/vm/interpreter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 6e7d28a0ce25..3bc38d9ade34 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -18,6 +18,7 @@ package vm import ( "fmt" + "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -50,7 +51,7 @@ func (ctx *ScopeContext) MemoryData() []byte { if ctx.Memory == nil { return nil } - return ctx.Memory.Data() + return slices.Clone(ctx.Memory.Data()) } // StackData returns the stack data. Callers must not modify the contents From fdfc2c9d03aac8f89e5e96c8be591fceb21f3473 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Tue, 16 Jul 2024 11:07:59 +0200 Subject: [PATCH 4/8] Revert "clone memory in `ScopeContext.MemoryData`" This reverts commit c59e9e5e6b28240093583a9a9f16bc060c8b1f7c. --- core/vm/interpreter.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 3bc38d9ade34..6e7d28a0ce25 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -18,7 +18,6 @@ package vm import ( "fmt" - "slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" @@ -51,7 +50,7 @@ func (ctx *ScopeContext) MemoryData() []byte { if ctx.Memory == nil { return nil } - return slices.Clone(ctx.Memory.Data()) + return ctx.Memory.Data() } // StackData returns the stack data. Callers must not modify the contents From 32a04d0bb376767241a63f9d7883c5036706a819 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Tue, 16 Jul 2024 11:12:45 +0200 Subject: [PATCH 5/8] copy memory regions in call and return ops --- core/vm/instructions.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 9ec454464363..8329886e98e3 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -672,7 +672,7 @@ func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get the arguments from the memory. - args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) if interpreter.readOnly && !value.IsZero() { return nil, ErrWriteProtection @@ -708,7 +708,7 @@ func opCallCode(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([ addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get arguments from the memory. - args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) if !value.IsZero() { gas += params.CallStipend @@ -741,7 +741,7 @@ func opDelegateCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext addr, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get arguments from the memory. - args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) ret, returnGas, err := interpreter.evm.DelegateCall(scope.Contract, toAddr, args, gas) if err != nil { @@ -770,7 +770,7 @@ func opStaticCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) addr, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get arguments from the memory. - args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) ret, returnGas, err := interpreter.evm.StaticCall(scope.Contract, toAddr, args, gas) if err != nil { @@ -791,14 +791,14 @@ func opStaticCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) func opReturn(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { offset, size := scope.Stack.pop(), scope.Stack.pop() - ret := scope.Memory.GetPtr(int64(offset.Uint64()), int64(size.Uint64())) + ret := scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) return ret, errStopToken } func opRevert(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { offset, size := scope.Stack.pop(), scope.Stack.pop() - ret := scope.Memory.GetPtr(int64(offset.Uint64()), int64(size.Uint64())) + ret := scope.Memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64())) interpreter.returnData = ret return ret, ErrExecutionReverted From fdee681d0acae761563ecf973180c26882a627a6 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Tue, 16 Jul 2024 17:25:29 +0200 Subject: [PATCH 6/8] copy input and output data before calling tracer --- core/vm/evm.go | 17 +++++++++-------- core/vm/instructions.go | 8 ++++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 1944189b5da2..40db945ae683 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -17,6 +17,7 @@ package vm import ( + "bytes" "errors" "math/big" "sync/atomic" @@ -190,9 +191,9 @@ func (evm *EVM) Interpreter() *EVMInterpreter { func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { // Capture the tracer start/end events in debug mode if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALL, caller.Address(), addr, input, gas, value.ToBig()) + evm.captureBegin(evm.depth, CALL, caller.Address(), addr, bytes.Clone(input), gas, value.ToBig()) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) }(gas) } // Fail if we're trying to execute above the call depth limit @@ -275,9 +276,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALLCODE, caller.Address(), addr, input, gas, value.ToBig()) + evm.captureBegin(evm.depth, CALLCODE, caller.Address(), addr, bytes.Clone(input), gas, value.ToBig()) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) }(gas) } // Fail if we're trying to execute above the call depth limit @@ -333,9 +334,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // that caller is something other than a Contract. parent := caller.(*Contract) // DELEGATECALL inherits value from parent call - evm.captureBegin(evm.depth, DELEGATECALL, caller.Address(), addr, input, gas, parent.value.ToBig()) + evm.captureBegin(evm.depth, DELEGATECALL, caller.Address(), addr, bytes.Clone(input), gas, parent.value.ToBig()) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) }(gas) } // Fail if we're trying to execute above the call depth limit @@ -377,9 +378,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, STATICCALL, caller.Address(), addr, input, gas, nil) + evm.captureBegin(evm.depth, STATICCALL, caller.Address(), addr, bytes.Clone(input), gas, nil) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) + evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) }(gas) } // Fail if we're trying to execute above the call depth limit diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 8329886e98e3..01109e285163 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -672,7 +672,7 @@ func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get the arguments from the memory. - args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) if interpreter.readOnly && !value.IsZero() { return nil, ErrWriteProtection @@ -708,7 +708,7 @@ func opCallCode(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([ addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get arguments from the memory. - args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) if !value.IsZero() { gas += params.CallStipend @@ -741,7 +741,7 @@ func opDelegateCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext addr, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get arguments from the memory. - args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) ret, returnGas, err := interpreter.evm.DelegateCall(scope.Contract, toAddr, args, gas) if err != nil { @@ -770,7 +770,7 @@ func opStaticCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) addr, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop() toAddr := common.Address(addr.Bytes20()) // Get arguments from the memory. - args := scope.Memory.GetCopy(int64(inOffset.Uint64()), int64(inSize.Uint64())) + args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64())) ret, returnGas, err := interpreter.evm.StaticCall(scope.Contract, toAddr, args, gas) if err != nil { From 8fc38b268227e9ef5cfdcda044ad8073848d3948 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Tue, 16 Jul 2024 21:11:14 +0200 Subject: [PATCH 7/8] don't copy input, output --- core/vm/evm.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/core/vm/evm.go b/core/vm/evm.go index 40db945ae683..1944189b5da2 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -17,7 +17,6 @@ package vm import ( - "bytes" "errors" "math/big" "sync/atomic" @@ -191,9 +190,9 @@ func (evm *EVM) Interpreter() *EVMInterpreter { func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { // Capture the tracer start/end events in debug mode if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALL, caller.Address(), addr, bytes.Clone(input), gas, value.ToBig()) + evm.captureBegin(evm.depth, CALL, caller.Address(), addr, input, gas, value.ToBig()) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) }(gas) } // Fail if we're trying to execute above the call depth limit @@ -276,9 +275,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, CALLCODE, caller.Address(), addr, bytes.Clone(input), gas, value.ToBig()) + evm.captureBegin(evm.depth, CALLCODE, caller.Address(), addr, input, gas, value.ToBig()) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) }(gas) } // Fail if we're trying to execute above the call depth limit @@ -334,9 +333,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // that caller is something other than a Contract. parent := caller.(*Contract) // DELEGATECALL inherits value from parent call - evm.captureBegin(evm.depth, DELEGATECALL, caller.Address(), addr, bytes.Clone(input), gas, parent.value.ToBig()) + evm.captureBegin(evm.depth, DELEGATECALL, caller.Address(), addr, input, gas, parent.value.ToBig()) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) }(gas) } // Fail if we're trying to execute above the call depth limit @@ -378,9 +377,9 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) { // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { - evm.captureBegin(evm.depth, STATICCALL, caller.Address(), addr, bytes.Clone(input), gas, nil) + evm.captureBegin(evm.depth, STATICCALL, caller.Address(), addr, input, gas, nil) defer func(startGas uint64) { - evm.captureEnd(evm.depth, startGas, leftOverGas, bytes.Clone(ret), err) + evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) }(gas) } // Fail if we're trying to execute above the call depth limit From 5ce0a681ce10bdd7bb48a8f9f13ce492d4aa7767 Mon Sep 17 00:00:00 2001 From: lmittmann Date: Wed, 31 Jul 2024 09:05:55 +0200 Subject: [PATCH 8/8] added RETURN benchmark --- core/vm/runtime/runtime_test.go | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 04abc5480eac..0c6df9992cb3 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -17,9 +17,11 @@ package runtime import ( + "encoding/binary" "fmt" "math/big" "os" + "strconv" "strings" "testing" @@ -213,6 +215,41 @@ func BenchmarkEVM_CREATE2_1200(bench *testing.B) { benchmarkEVM_Create(bench, "5b5862124f80600080f5600152600056") } +func BenchmarkEVM_RETURN(b *testing.B) { + // returns a contract that returns a zero-byte slice of len size + returnContract := func(size uint64) []byte { + contract := []byte{ + byte(vm.PUSH8), 0, 0, 0, 0, 0, 0, 0, 0, // PUSH8 0xXXXXXXXXXXXXXXXX + byte(vm.PUSH0), // PUSH0 + byte(vm.RETURN), // RETURN + } + binary.BigEndian.PutUint64(contract[1:], size) + return contract + } + + state, _ := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + contractAddr := common.BytesToAddress([]byte("contract")) + + for _, n := range []uint64{1_000, 10_000, 100_000, 1_000_000} { + b.Run(strconv.FormatUint(n, 10), func(b *testing.B) { + b.ReportAllocs() + + contractCode := returnContract(n) + state.SetCode(contractAddr, contractCode) + + for i := 0; i < b.N; i++ { + ret, _, err := Call(contractAddr, []byte{}, &Config{State: state}) + if err != nil { + b.Fatal(err) + } + if uint64(len(ret)) != n { + b.Fatalf("expected return size %d, got %d", n, len(ret)) + } + } + }) + } +} + func fakeHeader(n uint64, parentHash common.Hash) *types.Header { header := types.Header{ Coinbase: common.HexToAddress("0x00000000000000000000000000000000deadbeef"),