core/vm: reduce allocations on function call path

This commit is contained in:
Ömer Faruk IRMAK 2025-07-23 14:30:28 +03:00
parent c3ef6c77c2
commit bdcaeb299c
7 changed files with 74 additions and 68 deletions

View file

@ -47,12 +47,12 @@ type Contract struct {
}
// NewContract returns a new contract environment for the execution of EVM.
func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache) *Contract {
func NewContract(caller common.Address, address common.Address, value *uint256.Int, gas uint64, jumpDests JumpDestCache) Contract {
// Initialize the jump analysis cache if it's nil, mostly for tests
if jumpDests == nil {
jumpDests = newMapJumpDests()
}
return &Contract{
return Contract{
caller: caller,
address: address,
jumpDests: jumpDests,

View file

@ -130,6 +130,8 @@ type EVM struct {
readOnly bool // Whether to throw on stateful modifications
returnData []byte // Last CALL's return data for subsequent reuse
ScopeContext // Current Run context
}
// NewEVM constructs an EVM instance with the supplied block context, state
@ -294,7 +296,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
contract := NewContract(caller, addr, value, gas, evm.jumpDests)
contract.IsSystemCall = isSystemCall(caller)
contract.SetCallCode(evm.resolveCodeHash(addr), code)
ret, err = evm.Run(contract, input, false)
ret, err = evm.Run(&contract, input, false)
gas = contract.Gas
}
}
@ -353,7 +355,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
ret, err = evm.Run(&contract, input, false)
gas = contract.Gas
}
if err != nil {
@ -397,7 +399,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
// Note: The value refers to the original value from the parent call.
contract := NewContract(originCaller, caller, value, gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))
ret, err = evm.Run(contract, input, false)
ret, err = evm.Run(&contract, input, false)
gas = contract.Gas
}
if err != nil {
@ -452,7 +454,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in Homestead this also counts for code storage gas errors.
ret, err = evm.Run(contract, input, true)
ret, err = evm.Run(&contract, input, true)
gas = contract.Gas
}
if err != nil {
@ -560,7 +562,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas uint64, value *ui
contract.SetCallCode(common.Hash{}, code)
contract.IsDeployment = true
ret, err = evm.initNewContract(contract, address)
ret, err = evm.initNewContract(&contract, address)
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {

View file

@ -107,7 +107,7 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu
expected := new(uint256.Int).SetBytes(common.Hex2Bytes(test.Expected))
stack.push(x)
stack.push(y)
opFn(&pc, evm, &ScopeContext{nil, stack, nil})
opFn(&pc, evm, &ScopeContext{Stack: stack})
if len(stack.data) != 1 {
t.Errorf("Expected one item on stack after %v, got %d: ", name, len(stack.data))
}
@ -221,7 +221,7 @@ func TestAddMod(t *testing.T) {
stack.push(z)
stack.push(y)
stack.push(x)
opAddmod(&pc, evm, &ScopeContext{nil, stack, nil})
opAddmod(&pc, evm, &ScopeContext{Stack: stack})
actual := stack.pop()
if actual.Cmp(expected) != 0 {
t.Errorf("Testcase %d, expected %x, got %x", i, expected, actual)
@ -247,7 +247,7 @@ func TestWriteExpectedValues(t *testing.T) {
y := new(uint256.Int).SetBytes(common.Hex2Bytes(param.y))
stack.push(x)
stack.push(y)
opFn(&pc, evm, &ScopeContext{nil, stack, nil})
opFn(&pc, evm, &ScopeContext{Stack: stack})
actual := stack.pop()
result[i] = TwoOperandTestcase{param.x, param.y, fmt.Sprintf("%064x", actual)}
}
@ -283,7 +283,7 @@ func opBenchmark(bench *testing.B, op executionFunc, args ...string) {
var (
evm = NewEVM(BlockContext{}, nil, params.TestChainConfig, Config{})
stack = newstack()
scope = &ScopeContext{nil, stack, nil}
scope = &ScopeContext{Stack: stack}
)
// convert args
intArgs := make([]*uint256.Int, len(args))
@ -528,13 +528,13 @@ func TestOpMstore(t *testing.T) {
v := "abcdef00000000000000abba000000000deaf000000c0de00100000000133700"
stack.push(new(uint256.Int).SetBytes(common.Hex2Bytes(v)))
stack.push(new(uint256.Int))
opMstore(&pc, evm, &ScopeContext{mem, stack, nil})
opMstore(&pc, evm, &ScopeContext{Memory: mem, Stack: stack})
if got := common.Bytes2Hex(mem.GetCopy(0, 32)); got != v {
t.Fatalf("Mstore fail, got %v, expected %v", got, v)
}
stack.push(new(uint256.Int).SetUint64(0x1))
stack.push(new(uint256.Int))
opMstore(&pc, evm, &ScopeContext{mem, stack, nil})
opMstore(&pc, evm, &ScopeContext{Memory: mem, Stack: stack})
if common.Bytes2Hex(mem.GetCopy(0, 32)) != "0000000000000000000000000000000000000000000000000000000000000001" {
t.Fatalf("Mstore failed to overwrite previous value")
}
@ -555,7 +555,7 @@ func BenchmarkOpMstore(bench *testing.B) {
for i := 0; i < bench.N; i++ {
stack.push(value)
stack.push(memStart)
opMstore(&pc, evm, &ScopeContext{mem, stack, nil})
opMstore(&pc, evm, &ScopeContext{Memory: mem, Stack: stack})
}
}
@ -568,7 +568,7 @@ func TestOpTstore(t *testing.T) {
caller = common.Address{}
to = common.Address{1}
contract = NewContract(caller, to, new(uint256.Int), 0, nil)
scopeContext = ScopeContext{mem, stack, contract}
scopeContext = ScopeContext{Memory: mem, Stack: stack, Contract: contract}
value = common.Hex2Bytes("abcdef00000000000000abba000000000deaf000000c0de00100000000133700")
)
@ -613,7 +613,7 @@ func BenchmarkOpKeccak256(bench *testing.B) {
for i := 0; i < bench.N; i++ {
stack.push(uint256.NewInt(32))
stack.push(start)
opKeccak256(&pc, evm, &ScopeContext{mem, stack, nil})
opKeccak256(&pc, evm, &ScopeContext{Memory: mem, Stack: stack})
}
}
@ -707,7 +707,7 @@ func TestRandom(t *testing.T) {
stack = newstack()
pc = uint64(0)
)
opRandom(&pc, evm, &ScopeContext{nil, stack, nil})
opRandom(&pc, evm, &ScopeContext{Stack: stack})
if len(stack.data) != 1 {
t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data))
}
@ -749,7 +749,7 @@ func TestBlobHash(t *testing.T) {
)
evm.SetTxContext(TxContext{BlobHashes: tt.hashes})
stack.push(uint256.NewInt(tt.idx))
opBlobHash(&pc, evm, &ScopeContext{nil, stack, nil})
opBlobHash(&pc, evm, &ScopeContext{Stack: stack})
if len(stack.data) != 1 {
t.Errorf("Expected one item on stack after %v, got %d: ", tt.name, len(stack.data))
}
@ -889,7 +889,7 @@ func TestOpMCopy(t *testing.T) {
mem.Resize(memorySize)
}
// Do the copy
opMcopy(&pc, evm, &ScopeContext{mem, stack, nil})
opMcopy(&pc, evm, &ScopeContext{Memory: mem, Stack: stack})
want := common.FromHex(strings.ReplaceAll(tc.want, " ", ""))
if have := mem.store; !bytes.Equal(want, have) {
t.Errorf("case %d: \nwant: %#x\nhave: %#x\n", i, want, have)
@ -911,7 +911,7 @@ func TestPush(t *testing.T) {
scope := &ScopeContext{
Memory: nil,
Stack: newstack(),
Contract: &Contract{
Contract: Contract{
Code: code,
},
}

View file

@ -35,12 +35,13 @@ type Config struct {
StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
}
// ScopeContext contains the things that are per-call, such as stack and memory,
// but not transients like pc and gas
// ScopeContext contains the things that are per-call
type ScopeContext struct {
pc uint64
Memory *Memory
Stack *Stack
Contract *Contract
Contract Contract
}
// MemoryData returns the underlying memory slice. Callers must not modify the contents
@ -94,9 +95,30 @@ func (ctx *ScopeContext) ContractCode() []byte {
// considered a revert-and-consume-all-gas operation except for
// ErrExecutionReverted which means revert-and-keep-gas-left.
func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
// Don't bother with the execution if there's no code.
if len(contract.Code) == 0 {
return nil, nil
}
// Increment the call depth which is restricted to 1024
prevContext := evm.ScopeContext
evm.ScopeContext = ScopeContext{
pc: 0,
Memory: NewMemory(),
Stack: newstack(),
Contract: *contract,
}
evm.depth++
defer func() { evm.depth-- }()
defer func() {
evm.depth--
*contract = evm.Contract
// Don't move this deferred function, it's placed before the OnOpcode-deferred method,
// so that it gets executed _after_: the OnOpcode needs the stacks before
// they are returned to the pools
returnStack(evm.Stack)
evm.Memory.Free()
evm.ScopeContext = prevContext
}()
// Make sure the readOnly is only set if we aren't in readOnly yet.
// This also makes sure that the readOnly flag isn't removed for child calls.
@ -109,25 +131,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// as every returning call will return new data anyway.
evm.returnData = nil
// Don't bother with the execution if there's no code.
if len(contract.Code) == 0 {
return nil, nil
}
var (
op OpCode // current opcode
jumpTable *JumpTable = evm.table
mem = NewMemory() // bound memory
stack = newstack() // local stack
callContext = &ScopeContext{
Memory: mem,
Stack: stack,
Contract: contract,
}
op OpCode // current opcode
jumpTable *JumpTable = evm.table
// For optimisation reason we're using uint64 as the program counter.
// It's theoretically possible to go above 2^64. The YP defines the PC
// to be uint256. Practically much less so feasible.
pc = uint64(0) // program counter
cost uint64
// copies used by tracer
pcCopy uint64 // needed for the deferred EVMLogger
@ -137,14 +146,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
debug = evm.Config.Tracer != nil
isEIP4762 = evm.chainRules.IsEIP4762
)
// Don't move this deferred function, it's placed before the OnOpcode-deferred method,
// so that it gets executed _after_: the OnOpcode needs the stacks before
// they are returned to the pools
defer func() {
returnStack(stack)
mem.Free()
}()
contract.Input = input
evm.Contract.Input = input
if debug {
defer func() { // this deferred method handles exit-with-error
@ -152,10 +154,10 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
return
}
if !logged && evm.Config.Tracer.OnOpcode != nil {
evm.Config.Tracer.OnOpcode(pcCopy, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err))
evm.Config.Tracer.OnOpcode(pcCopy, byte(op), gasCopy, cost, &evm.ScopeContext, evm.returnData, evm.depth, VMErrorFromErr(err))
}
if logged && evm.Config.Tracer.OnFault != nil {
evm.Config.Tracer.OnFault(pcCopy, byte(op), gasCopy, cost, callContext, evm.depth, VMErrorFromErr(err))
evm.Config.Tracer.OnFault(pcCopy, byte(op), gasCopy, cost, &evm.ScopeContext, evm.depth, VMErrorFromErr(err))
}
}()
}
@ -167,14 +169,14 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
for {
if debug {
// Capture pre-execution values for tracing.
logged, pcCopy, gasCopy = false, pc, contract.Gas
logged, pcCopy, gasCopy = false, evm.pc, evm.Contract.Gas
}
if isEIP4762 && !contract.IsDeployment && !contract.IsSystemCall {
// if the PC ends up in a new "chunk" of verkleized code, charge the
// associated costs.
contractAddr := contract.Address()
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, pc, 1, uint64(len(contract.Code)), false, contract.Gas)
consumed, wanted := evm.TxContext.AccessEvents.CodeChunksRangeGas(contractAddr, evm.pc, 1, uint64(len(contract.Code)), false, contract.Gas)
contract.UseGas(consumed, evm.Config.Tracer, tracing.GasChangeWitnessCodeChunk)
if consumed < wanted {
return nil, ErrOutOfGas
@ -183,20 +185,20 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// Get the operation from the jump table and validate the stack to ensure there are
// enough stack items available to perform the operation.
op = contract.GetOp(pc)
op = evm.Contract.GetOp(evm.pc)
operation := jumpTable[op]
cost = operation.constantGas // For tracing
// Validate stack
if sLen := stack.len(); sLen < operation.minStack {
if sLen := evm.Stack.len(); sLen < operation.minStack {
return nil, &ErrStackUnderflow{stackLen: sLen, required: operation.minStack}
} else if sLen > operation.maxStack {
return nil, &ErrStackOverflow{stackLen: sLen, limit: operation.maxStack}
}
// for tracing: this gas consumption event is emitted below in the debug section.
if contract.Gas < cost {
if evm.Contract.Gas < cost {
return nil, ErrOutOfGas
} else {
contract.Gas -= cost
evm.Contract.Gas -= cost
}
// All ops with a dynamic memory usage also has a dynamic gas cost.
@ -207,7 +209,7 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// Memory check needs to be done prior to evaluating the dynamic gas portion,
// to detect calculation overflows
if operation.memorySize != nil {
memSize, overflow := operation.memorySize(stack)
memSize, overflow := operation.memorySize(evm.Stack)
if overflow {
return nil, ErrGasUintOverflow
}
@ -220,16 +222,16 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// Consume the gas and return an error if not enough gas is available.
// cost is explicitly set so that the capture state defer method can get the proper cost
var dynamicCost uint64
dynamicCost, err = operation.dynamicGas(evm, contract, stack, mem, memorySize)
dynamicCost, err = operation.dynamicGas(evm, &evm.Contract, evm.Stack, evm.Memory, memorySize)
cost += dynamicCost // for tracing
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrOutOfGas, err)
}
// for tracing: this gas consumption event is emitted below in the debug section.
if contract.Gas < dynamicCost {
if evm.Contract.Gas < dynamicCost {
return nil, ErrOutOfGas
} else {
contract.Gas -= dynamicCost
evm.Contract.Gas -= dynamicCost
}
}
@ -239,20 +241,20 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode)
}
if evm.Config.Tracer.OnOpcode != nil {
evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err))
evm.Config.Tracer.OnOpcode(evm.pc, byte(op), gasCopy, cost, &evm.ScopeContext, evm.returnData, evm.depth, VMErrorFromErr(err))
logged = true
}
}
if memorySize > 0 {
mem.Resize(memorySize)
evm.Memory.Resize(memorySize)
}
// execute the operation
res, err = operation.execute(&pc, evm, callContext)
res, err = operation.execute(&evm.pc, evm, &evm.ScopeContext)
if err != nil {
break
}
pc++
evm.pc++
}
if err == errStopToken {

View file

@ -91,6 +91,6 @@ func BenchmarkInterpreter(b *testing.B) {
gasSStoreEIP3529 = makeGasSStoreFunc(params.SstoreClearsScheduleRefundEIP3529)
b.ResetTimer()
for i := 0; i < b.N; i++ {
gasSStoreEIP3529(evm, contract, stack, mem, 1234)
gasSStoreEIP3529(evm, &contract, stack, mem, 1234)
}
}

View file

@ -65,7 +65,7 @@ func runTrace(tracer *tracers.Tracer, vmctx *vmContext, chaincfg *params.ChainCo
tracer.OnTxStart(evm.GetVMContext(), types.NewTx(&types.LegacyTx{Gas: gasLimit, GasPrice: vmctx.txCtx.GasPrice}), contract.Caller())
tracer.OnEnter(0, byte(vm.CALL), contract.Caller(), contract.Address(), []byte{}, startGas, value.ToBig())
ret, err := evm.Run(contract, []byte{}, false)
ret, err := evm.Run(&contract, []byte{}, false)
tracer.OnExit(0, ret, startGas-contract.Gas, err, true)
// Rest gas assumes no refund
tracer.OnTxEnd(&types.Receipt{GasUsed: gasLimit - contract.Gas}, nil)
@ -182,8 +182,9 @@ func TestHaltBetweenSteps(t *testing.T) {
if err != nil {
t.Fatal(err)
}
contract := vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), 0, nil)
scope := &vm.ScopeContext{
Contract: vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), 0, nil),
Contract: contract,
}
evm := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, &dummyStatedb{}, chainConfig, vm.Config{Tracer: tracer.Hooks})
evm.SetTxContext(vm.TxContext{GasPrice: big.NewInt(1)})
@ -280,8 +281,9 @@ func TestEnterExit(t *testing.T) {
if err != nil {
t.Fatal(err)
}
contract := vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), 0, nil)
scope := &vm.ScopeContext{
Contract: vm.NewContract(common.Address{}, common.Address{}, uint256.NewInt(0), 0, nil),
Contract: contract,
}
tracer.OnEnter(1, byte(vm.CALL), scope.Contract.Caller(), scope.Contract.Address(), []byte{}, 1000, new(big.Int))
tracer.OnExit(1, []byte{}, 400, nil, false)

View file

@ -52,7 +52,7 @@ func TestStoreCapture(t *testing.T) {
contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)}
var index common.Hash
logger.OnTxStart(evm.GetVMContext(), nil, common.Address{})
_, err := evm.Run(contract, []byte{}, false)
_, err := evm.Run(&contract, []byte{}, false)
if err != nil {
t.Fatal(err)
}