core/vm: use a callcontext struct (#20761)

* core/vm: use a callcontext struct

* core/vm: fix tests

* core/vm/runtime: benchmark

* core/vm: make intpool push inlineable, unexpose callcontext
This commit is contained in:
Martin Holst Swende 2020-04-07 11:45:21 +02:00 committed by GitHub
parent 0bec6a43f6
commit 8dc8941551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 349 additions and 292 deletions

@ -60,9 +60,9 @@ func enable1884(jt *JumpTable) {
} }
} }
func opSelfBalance(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { func opSelfBalance(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
balance := interpreter.intPool.get().Set(interpreter.evm.StateDB.GetBalance(contract.Address())) balance := interpreter.intPool.get().Set(interpreter.evm.StateDB.GetBalance(callContext.contract.Address()))
stack.push(balance) callContext.stack.push(balance)
return nil, nil return nil, nil
} }
@ -80,9 +80,9 @@ func enable1344(jt *JumpTable) {
} }
// opChainID implements CHAINID opcode // opChainID implements CHAINID opcode
func opChainID(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { func opChainID(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error) {
chainId := interpreter.intPool.get().Set(interpreter.evm.chainConfig.ChainID) chainId := interpreter.intPool.get().Set(interpreter.evm.chainConfig.ChainID)
stack.push(chainId) callContext.stack.push(chainId)
return nil, nil return nil, nil
} }

File diff suppressed because it is too large Load Diff

@ -109,7 +109,7 @@ func testTwoOperandOp(t *testing.T, tests []TwoOperandTestcase, opFn executionFu
expected := new(big.Int).SetBytes(common.Hex2Bytes(test.Expected)) expected := new(big.Int).SetBytes(common.Hex2Bytes(test.Expected))
stack.push(x) stack.push(x)
stack.push(y) stack.push(y)
opFn(&pc, evmInterpreter, nil, nil, stack) opFn(&pc, evmInterpreter, &callCtx{nil, stack, nil})
actual := stack.pop() actual := stack.pop()
if actual.Cmp(expected) != 0 { if actual.Cmp(expected) != 0 {
@ -223,7 +223,7 @@ func getResult(args []*twoOperandParams, opFn executionFunc) []TwoOperandTestcas
y := new(big.Int).SetBytes(common.Hex2Bytes(param.y)) y := new(big.Int).SetBytes(common.Hex2Bytes(param.y))
stack.push(x) stack.push(x)
stack.push(y) stack.push(y)
opFn(&pc, interpreter, nil, nil, stack) opFn(&pc, interpreter, &callCtx{nil, stack, nil})
actual := stack.pop() actual := stack.pop()
result[i] = TwoOperandTestcase{param.x, param.y, fmt.Sprintf("%064x", actual)} result[i] = TwoOperandTestcase{param.x, param.y, fmt.Sprintf("%064x", actual)}
} }
@ -260,7 +260,7 @@ func TestJsonTestcases(t *testing.T) {
} }
} }
func opBenchmark(bench *testing.B, op func(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error), args ...string) { func opBenchmark(bench *testing.B, op executionFunc, args ...string) {
var ( var (
env = NewEVM(Context{}, nil, params.TestChainConfig, Config{}) env = NewEVM(Context{}, nil, params.TestChainConfig, Config{})
stack = newstack() stack = newstack()
@ -281,7 +281,7 @@ func opBenchmark(bench *testing.B, op func(pc *uint64, interpreter *EVMInterpret
a := new(big.Int).SetBytes(arg) a := new(big.Int).SetBytes(arg)
stack.push(a) stack.push(a)
} }
op(&pc, evmInterpreter, nil, nil, stack) op(&pc, evmInterpreter, &callCtx{nil, stack, nil})
stack.pop() stack.pop()
} }
poolOfIntPools.put(evmInterpreter.intPool) poolOfIntPools.put(evmInterpreter.intPool)
@ -509,12 +509,12 @@ func TestOpMstore(t *testing.T) {
pc := uint64(0) pc := uint64(0)
v := "abcdef00000000000000abba000000000deaf000000c0de00100000000133700" v := "abcdef00000000000000abba000000000deaf000000c0de00100000000133700"
stack.pushN(new(big.Int).SetBytes(common.Hex2Bytes(v)), big.NewInt(0)) stack.pushN(new(big.Int).SetBytes(common.Hex2Bytes(v)), big.NewInt(0))
opMstore(&pc, evmInterpreter, nil, mem, stack) opMstore(&pc, evmInterpreter, &callCtx{mem, stack, nil})
if got := common.Bytes2Hex(mem.GetCopy(0, 32)); got != v { if got := common.Bytes2Hex(mem.GetCopy(0, 32)); got != v {
t.Fatalf("Mstore fail, got %v, expected %v", got, v) t.Fatalf("Mstore fail, got %v, expected %v", got, v)
} }
stack.pushN(big.NewInt(0x1), big.NewInt(0)) stack.pushN(big.NewInt(0x1), big.NewInt(0))
opMstore(&pc, evmInterpreter, nil, mem, stack) opMstore(&pc, evmInterpreter, &callCtx{mem, stack, nil})
if common.Bytes2Hex(mem.GetCopy(0, 32)) != "0000000000000000000000000000000000000000000000000000000000000001" { if common.Bytes2Hex(mem.GetCopy(0, 32)) != "0000000000000000000000000000000000000000000000000000000000000001" {
t.Fatalf("Mstore failed to overwrite previous value") t.Fatalf("Mstore failed to overwrite previous value")
} }
@ -539,7 +539,7 @@ func BenchmarkOpMstore(bench *testing.B) {
bench.ResetTimer() bench.ResetTimer()
for i := 0; i < bench.N; i++ { for i := 0; i < bench.N; i++ {
stack.pushN(value, memStart) stack.pushN(value, memStart)
opMstore(&pc, evmInterpreter, nil, mem, stack) opMstore(&pc, evmInterpreter, &callCtx{mem, stack, nil})
} }
poolOfIntPools.put(evmInterpreter.intPool) poolOfIntPools.put(evmInterpreter.intPool)
} }
@ -560,7 +560,7 @@ func BenchmarkOpSHA3(bench *testing.B) {
bench.ResetTimer() bench.ResetTimer()
for i := 0; i < bench.N; i++ { for i := 0; i < bench.N; i++ {
stack.pushN(big.NewInt(32), start) stack.pushN(big.NewInt(32), start)
opSha3(&pc, evmInterpreter, nil, mem, stack) opSha3(&pc, evmInterpreter, &callCtx{mem, stack, nil})
} }
poolOfIntPools.put(evmInterpreter.intPool) poolOfIntPools.put(evmInterpreter.intPool)
} }

@ -63,6 +63,14 @@ type Interpreter interface {
CanRun([]byte) bool CanRun([]byte) bool
} }
// callCtx contains the things that are per-call, such as stack and memory,
// but not transients like pc and gas
type callCtx struct {
memory *Memory
stack *Stack
contract *Contract
}
// keccakState wraps sha3.state. In addition to the usual hash methods, it also supports // keccakState wraps sha3.state. In addition to the usual hash methods, it also supports
// Read to get a variable amount of data from the hash state. Read is faster than Sum // Read to get a variable amount of data from the hash state. Read is faster than Sum
// because it doesn't copy the internal state, but also modifies the internal state. // because it doesn't copy the internal state, but also modifies the internal state.
@ -163,6 +171,11 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
op OpCode // current opcode op OpCode // current opcode
mem = NewMemory() // bound memory mem = NewMemory() // bound memory
stack = newstack() // local stack stack = newstack() // local stack
callContext = &callCtx{
memory: mem,
stack: stack,
contract: contract,
}
// For optimisation reason we're using uint64 as the program counter. // 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 // It's theoretically possible to go above 2^64. The YP defines the PC
// to be uint256. Practically much less so feasible. // to be uint256. Practically much less so feasible.
@ -194,7 +207,12 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
// explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during // explicit STOP, RETURN or SELFDESTRUCT is executed, an error occurred during
// the execution of one of the operations or until the done flag is set by the // the execution of one of the operations or until the done flag is set by the
// parent context. // parent context.
for atomic.LoadInt32(&in.evm.abort) == 0 { steps := 0
for {
steps++
if steps%1000 == 0 && atomic.LoadInt32(&in.evm.abort) != 0 {
break
}
if in.cfg.Debug { if in.cfg.Debug {
// Capture pre-execution values for tracing. // Capture pre-execution values for tracing.
logged, pcCopy, gasCopy = false, pc, contract.Gas logged, pcCopy, gasCopy = false, pc, contract.Gas
@ -267,7 +285,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
} }
// execute the operation // execute the operation
res, err = operation.execute(&pc, in, contract, mem, stack) res, err = operation.execute(&pc, in, callContext)
// verifyPool is a build flag. Pool verification makes sure the integrity // verifyPool is a build flag. Pool verification makes sure the integrity
// of the integer pool by comparing values to a default value. // of the integer pool by comparing values to a default value.
if verifyPool { if verifyPool {

@ -53,6 +53,17 @@ func (p *intPool) getZero() *big.Int {
return new(big.Int) return new(big.Int)
} }
// putOne returns an allocated big int to the pool to be later reused by get calls.
// Note, the values as saved as is; neither put nor get zeroes the ints out!
// As opposed to 'put' with variadic args, this method becomes inlined by the
// go compiler
func (p *intPool) putOne(i *big.Int) {
if len(p.pool.data) > poolLimit {
return
}
p.pool.push(i)
}
// put returns an allocated big int to the pool to be later reused by get calls. // put returns an allocated big int to the pool to be later reused by get calls.
// Note, the values as saved as is; neither put nor get zeroes the ints out! // Note, the values as saved as is; neither put nor get zeroes the ints out!
func (p *intPool) put(is ...*big.Int) { func (p *intPool) put(is ...*big.Int) {

@ -23,7 +23,7 @@ import (
) )
type ( type (
executionFunc func(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) executionFunc func(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([]byte, error)
gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64 gasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64
// memorySizeFunc returns the required size, and whether the operation overflowed a uint64 // memorySizeFunc returns the required size, and whether the operation overflowed a uint64
memorySizeFunc func(*Stack) (size uint64, overflow bool) memorySizeFunc func(*Stack) (size uint64, overflow bool)

@ -316,3 +316,31 @@ func TestBlockhash(t *testing.T) {
t.Errorf("suboptimal; too much chain iteration, expected %d, got %d", exp, got) t.Errorf("suboptimal; too much chain iteration, expected %d, got %d", exp, got)
} }
} }
// BenchmarkSimpleLoop test a pretty simple loop which loops
// 1M (1 048 575) times.
// Takes about 200 ms
func BenchmarkSimpleLoop(b *testing.B) {
// 0xfffff = 1048575 loops
code := []byte{
byte(vm.PUSH3), 0x0f, 0xff, 0xff,
byte(vm.JUMPDEST), // [ count ]
byte(vm.PUSH1), 1, // [count, 1]
byte(vm.SWAP1), // [1, count]
byte(vm.SUB), // [ count -1 ]
byte(vm.DUP1), // [ count -1 , count-1]
byte(vm.PUSH1), 4, // [count-1, count -1, label]
byte(vm.JUMPI), // [ 0 ]
byte(vm.STOP),
}
//tracer := vm.NewJSONLogger(nil, os.Stdout)
//Execute(code, nil, &Config{
// EVMConfig: vm.Config{
// Debug: true,
// Tracer: tracer,
// }})
for i := 0; i < b.N; i++ {
Execute(code, nil, nil)
}
}