From b48e9d7664d555cb9470d970b5ef4e06681ed10a Mon Sep 17 00:00:00 2001 From: Jeffrey Wilcke Date: Tue, 11 Oct 2016 17:58:41 +0200 Subject: [PATCH] core/vm: working on peephole optimisation --- core/vm/instructions.go | 9 +++ core/vm/opcodes.go | 4 + core/vm/program.go | 4 +- core/vm/program_optimiser.go | 152 +++++++++++++++++++++++++++++------ core/vm/program_util.go | 23 +++--- core/vm/segments.go | 38 +++++++-- core/vm/vm.go | 9 ++- 7 files changed, 195 insertions(+), 44 deletions(-) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 59fc4e02e3..100a7be21e 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -34,6 +34,8 @@ type programInstruction interface { Op() OpCode } +//[static memory], if calldata goto jump table + type instrFn func(instr instruction, pc *uint64, env *Environment, contract *Contract, memory *Memory, stack *Stack) type instruction struct { @@ -49,6 +51,13 @@ type instruction struct { returns bool } +func (instr instruction) String() string { + if instr.op == PUSH1 { + return fmt.Sprintf("PUSH %v", instr.data) + } + return fmt.Sprintf("%v", instr.op) +} + func jump(mapping map[uint64]uint64, destinations map[uint64]struct{}, contract *Contract, to *big.Int) (uint64, error) { if !validDest(destinations, to) { nop := contract.GetOp(to.Uint64()) diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 9d2b037a56..431e66ba30 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -195,6 +195,8 @@ const ( ) const ( + OPTIMISED OpCode = 0xd0 + iota + // 0xf0 range - closures CREATE OpCode = 0xf0 + iota CALL @@ -360,6 +362,8 @@ var opCodeToString = map[OpCode]string{ PUSH: "PUSH", DUP: "DUP", SWAP: "SWAP", + + OPTIMISED: "OPTIMISED", } func (o OpCode) String() string { diff --git a/core/vm/program.go b/core/vm/program.go index 66e2dcfbb3..f114777e7a 100644 --- a/core/vm/program.go +++ b/core/vm/program.go @@ -82,8 +82,6 @@ type Program struct { Id common.Hash // Id of the program status int32 // status should be accessed atomically - contract *Contract - instructions []programInstruction // instruction set mapping map[uint64]uint64 // real PC mapping to array indices destinations map[uint64]struct{} // cached jump destinations @@ -288,6 +286,8 @@ func CompileProgram(program *Program) { program.addInstr(op, pc, nil, nil) } } + // reset the program code. It's no longer required by the program itself. + //program.code = nil } func RunProgram(program *Program, env *Environment, contract *Contract, input []byte) ([]byte, error) { diff --git a/core/vm/program_optimiser.go b/core/vm/program_optimiser.go index 79c91df3e6..3095fe214c 100644 --- a/core/vm/program_optimiser.go +++ b/core/vm/program_optimiser.go @@ -17,51 +17,149 @@ package vm import ( + "fmt" "math/big" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" ) +type ( + fnId string + fnInfo struct { + gas uint64 + pos uint64 + } + jumpTableInstr map[fnId]fnInfo + arithSeg struct { + value *big.Int + gas uint64 + pcRange uint64 + } +) + +func (as arithSeg) do(program *Program, pc *uint64, env *Environment, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { + // Use the calculated gas. When insufficient gas is present, use all gas and return an + // Out Of Gas error + if !contract.UseGas(as.gas) { + return nil, OutOfGasError + } + + stack.push(new(big.Int).Set(as.value)) + + *pc += as.pcRange + return nil, nil +} +func (as arithSeg) halts() bool { return false } +func (as arithSeg) Op() OpCode { return OPTIMISED } +func (as arithSeg) String() string { return fmt.Sprintf("ARITH_SEG(%v: %v)", EXP, as.value) } + +func (jti jumpTableInstr) do(program *Program, pc *uint64, env *Environment, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { + fnId := fnId(stack.data[len(stack.data)-1].Bytes()) + fn, ok := jti[fnId] + glog.V(logger.Debug).Infof("function call: 0x%x (p=%d exist=%v)", fnId, fn.pos, ok) + *pc = fn.pos + + return nil, nil +} +func (jumpTableInstr) halts() bool { return false } +func (jumpTableInstr) Op() OpCode { return OPTIMISED } +func (jti jumpTableInstr) String() string { return fmt.Sprintf("FUNC_TABLE(%d)", len(jti)) } + // optimeProgram optimises a JIT program creating segments out of program // instructions. Currently covered are multi-pushes and static jumps func OptimiseProgram(program *Program) { - var load []instruction - - var ( - statsJump = 0 - statsOldPush = 0 - statsNewPush = 0 - ) - if glog.V(logger.Debug) { glog.Infof("optimising %x\n", program.Id[:4]) tstart := time.Now() defer func() { - glog.Infof("optimised %x done in %v with JMP: %d PSH: %d/%d\n", program.Id[:4], time.Since(tstart), statsJump, statsNewPush, statsOldPush) + glog.Infof("optimised %x done in %v\n", program.Id[:4], time.Since(tstart)) }() } - /* - code := Parse(program.code) - for _, test := range [][]OpCode{ - []OpCode{PUSH, PUSH, ADD}, - []OpCode{PUSH, PUSH, SUB}, - []OpCode{PUSH, PUSH, MUL}, - []OpCode{PUSH, PUSH, DIV}, - } { - matchCount := 0 - MatchFn(code, test, func(i int) bool { - matchCount++ - return true - }) - fmt.Printf("found %d match count on: %v\n", matchCount, test) - } - */ + code := Parse(program.code) + MatchFn(code, []OpCode{PUSH, PUSH, EXP}, func(i int) bool { + var ( + instr arithSeg + instrPos uint64 + ) + pushOp := code[i] + instrPos = pushOp.pc + + size := int64(program.code[pushOp.pc]) - int64(PUSH1) + 1 + instr.pcRange += uint64(size) + 1 // size + push instruction + exponent := getData([]byte(program.code), big.NewInt(int64(pushOp.pc+1)), big.NewInt(size)) + + pushOp = code[i+1] + size = int64(program.code[pushOp.pc]) - int64(PUSH1) + 1 + instr.pcRange += uint64(size) + 1 // size + push instruction + base := getData([]byte(program.code), big.NewInt(int64(pushOp.pc+1)), big.NewInt(size)) + + //instr.value = math.Exp(common.Bytes2Big(base), common.Bytes2Big(exponent)) + instr.value = new(big.Int).Exp(common.Bytes2Big(base), common.Bytes2Big(exponent), Pow256) + instr.gas = GasFastestStep64*2 + GasSlowStep64 + uint64(len(exponent))*ExpByteGas64 + + instr.pcRange++ + instr.pcRange-- + instr.pcRange-- + + program.instructions[program.mapping[instrPos]] = instr + + return true + }) + + funcTable, jumpStart := make(jumpTableInstr), uint64(0) + MatchFn(code, []OpCode{PUSH, DUP, EQ, PUSH, JUMPI}, func(i int) bool { + pushOp := code[i] + jumpStart = pushOp.pc + + size := int64(program.code[pushOp.pc]) - int64(PUSH1) + 1 + funcId := fnId(getData([]byte(program.code), big.NewInt(int64(pushOp.pc+1)), big.NewInt(size))) + + pushOp = code[i+3] + size = int64(program.code[pushOp.pc]) - int64(PUSH1) + 1 + position := common.Bytes2Big(getData([]byte(program.code), big.NewInt(int64(pushOp.pc+1)), big.NewInt(size))).Uint64() + glog.V(logger.Debug).Infof("jumpTable start : %x => %d - %d\n", funcId, position, jumpStart) + + // TODO set the right amount of gas + funcTable[funcId] = fnInfo{pos: program.mapping[position], gas: 0} + + return true + }) + + MatchFn(code, []OpCode{DUP, PUSH, EQ, PUSH, JUMPI}, func(i int) bool { + pushOp := code[i+1] + size := int64(program.code[pushOp.pc]) - int64(PUSH1) + 1 + funcId := fnId(getData([]byte(program.code), big.NewInt(int64(pushOp.pc+1)), big.NewInt(size))) + + pushOp = code[i+3] + size = int64(program.code[pushOp.pc]) - int64(PUSH1) + 1 + position := common.Bytes2Big(getData([]byte(program.code), big.NewInt(int64(pushOp.pc+1)), big.NewInt(size))).Uint64() + glog.V(logger.Debug).Infof("jumpTable entry: %x => %d\n", funcId, position) + + // TODO set the rigth amount of gas + funcTable[funcId] = fnInfo{pos: program.mapping[position], gas: 0} + + return true + }) + if len(funcTable) > 0 { + program.instructions[program.mapping[jumpStart]] = funcTable + } + + var ( + load []instruction + statsJump = 0 + statsOldPush = 0 + statsNewPush = 0 + ) for i := 0; i < len(program.instructions); i++ { - instr := program.instructions[i].(instruction) + instr, ok := program.instructions[i].(instruction) + if !ok { + continue + } switch { case instr.op.IsPush(): @@ -95,6 +193,8 @@ func OptimiseProgram(program *Program) { load = nil } } + glog.V(logger.Debug).Infof("optimised %d pushes as %d pushes\n", statsOldPush, statsNewPush) + glog.V(logger.Debug).Infof("optimised %d static jumps\n", statsJump) } // makePushSeg creates a new push segment from N amount of push instructions diff --git a/core/vm/program_util.go b/core/vm/program_util.go index 947dae88f4..067061abe9 100644 --- a/core/vm/program_util.go +++ b/core/vm/program_util.go @@ -16,23 +16,28 @@ package vm +type parsedOp struct { + op OpCode + pc uint64 +} + // Parse parses all opcodes from the given code byte slice. This function // performs no error checking and may return non-existing opcodes. -func Parse(code []byte) (opcodes []OpCode) { +func Parse(code []byte) (opcodes []parsedOp) { for pc := uint64(0); pc < uint64(len(code)); pc++ { op := OpCode(code[pc]) switch op { case PUSH1, PUSH2, PUSH3, PUSH4, PUSH5, PUSH6, PUSH7, PUSH8, PUSH9, PUSH10, PUSH11, PUSH12, PUSH13, PUSH14, PUSH15, PUSH16, PUSH17, PUSH18, PUSH19, PUSH20, PUSH21, PUSH22, PUSH23, PUSH24, PUSH25, PUSH26, PUSH27, PUSH28, PUSH29, PUSH30, PUSH31, PUSH32: a := uint64(op) - uint64(PUSH1) + 1 + opcodes = append(opcodes, parsedOp{PUSH, pc}) pc += a - opcodes = append(opcodes, PUSH) case DUP1, DUP2, DUP3, DUP4, DUP5, DUP6, DUP7, DUP8, DUP9, DUP10, DUP11, DUP12, DUP13, DUP14, DUP15, DUP16: - opcodes = append(opcodes, DUP) + opcodes = append(opcodes, parsedOp{DUP, pc}) case SWAP1, SWAP2, SWAP3, SWAP4, SWAP5, SWAP6, SWAP7, SWAP8, SWAP9, SWAP10, SWAP11, SWAP12, SWAP13, SWAP14, SWAP15, SWAP16: - opcodes = append(opcodes, SWAP) + opcodes = append(opcodes, parsedOp{SWAP, pc}) default: - opcodes = append(opcodes, op) + opcodes = append(opcodes, parsedOp{op, pc}) } } @@ -43,7 +48,7 @@ func Parse(code []byte) (opcodes []OpCode) { // an appropriate match. matcherFn yields the starting position in the input. // MatchFn will continue to search for a match until it reaches the end of the // buffer or if matcherFn return false. -func MatchFn(input, match []OpCode, matcherFn func(int) bool) { +func MatchFn(input []parsedOp, match []OpCode, matcherFn func(int) bool) { // short circuit if either input or match is empty or if the match is // greater than the input if len(input) == 0 || len(match) == 0 || len(match) > len(input) { @@ -51,11 +56,11 @@ func MatchFn(input, match []OpCode, matcherFn func(int) bool) { } main: - for i, op := range input[:len(input)+1-len(match)] { + for i, parsedOp := range input[:len(input)+1-len(match)] { // match first opcode and continue search - if op == match[0] { + if parsedOp.op == match[0] { for j := 1; j < len(match); j++ { - if input[i+j] != match[j] { + if input[i+j].op != match[j] { continue main } } diff --git a/core/vm/segments.go b/core/vm/segments.go index 76e662ea07..632ab221c5 100644 --- a/core/vm/segments.go +++ b/core/vm/segments.go @@ -16,7 +16,10 @@ package vm -import "math/big" +import ( + "fmt" + "math/big" +) type jumpSeg struct { pos uint64 @@ -34,8 +37,9 @@ func (j jumpSeg) do(vm *EVM, program *Program, pc *uint64, env *Environment, con *pc = j.pos return nil, nil } -func (s jumpSeg) halts() bool { return false } -func (s jumpSeg) Op() OpCode { return 0 } +func (s jumpSeg) halts() bool { return false } +func (s jumpSeg) Op() OpCode { return OPTIMISED } +func (s jumpSeg) String() string { return fmt.Sprintf("JUMP_SEG->%d", s.pos) } type pushSeg struct { data []*big.Int @@ -56,5 +60,29 @@ func (s pushSeg) do(vm *EVM, program *Program, pc *uint64, env *Environment, con return nil, nil } -func (s pushSeg) halts() bool { return false } -func (s pushSeg) Op() OpCode { return 0 } +func (s pushSeg) halts() bool { return false } +func (s pushSeg) Op() OpCode { return OPTIMISED } +func (s pushSeg) String() string { return fmt.Sprintf("PUSH_SEG(%d)", len(s.data)) } + +//CALLDATASIZE, ISZERO, PUSH2, JUMPI +//if len(calldata) > 0 { +// *pc = T.pos +//} + +//PUSH 224, PUSH 2, EXP, PUSH 0, CALLDATALOAD, DIV +//calldata[:4] + +//PUSH4, DUP2, EQ, PUSH2, JUMP +/* +if calldata[:4] == (PUSH4) +else if calldata[:4] == (PUSH2) ???? +else if calldata[:4] == (PUSH2) ???? + +type programJumpTable map[funcId]dest + +if len(calldata) > 0 { + if ppc, exist := programJumpTable[string(calldata[:4])]; exit { + *pc = ppc + } +} +*/ diff --git a/core/vm/vm.go b/core/vm/vm.go index dbd10c0d5e..6849c145c4 100644 --- a/core/vm/vm.go +++ b/core/vm/vm.go @@ -18,6 +18,7 @@ package vm import ( "fmt" + "os" "time" "github.com/ethereum/go-ethereum/common" @@ -116,24 +117,28 @@ func (evm *EVM) runProgram(program *Program, contract *Contract, input []byte) ( ) if glog.V(logger.Debug) { - glog.Infof("running JIT program %x\n", program.Id[:4]) + glog.Infof("running EVM program %x\n", program.Id[:4]) tstart := time.Now() defer func() { - glog.Infof("JIT program %x done. time: %v instrc: %v\n", program.Id[:4], time.Since(tstart), instrCount) + glog.Infof("EVM program %x done. time: %v instrc: %v\n", program.Id[:4], time.Since(tstart), instrCount) + os.Exit(1) }() } + fmt.Printf("%x\n", input) homestead := env.ChainConfig().IsHomestead(env.BlockNumber) for pc < uint64(len(program.instructions)) { instrCount++ instr := program.instructions[pc] + //fmt.Println(instr) if instr.Op() == DELEGATECALL && !homestead { return nil, fmt.Errorf("Invalid opcode 0x%x", instr.Op()) } ret, err := instr.do(evm, program, &pc, env, contract, mem, stack) if err != nil { + glog.Infoln("error executing EVM program for: '", instr.Op(), "' with: ", err) //gas := new(big.Int).SetUint64(contract.gas64) //evm.cfg.Tracer.CaptureState(evm.env, pc, instr.Op(), gas, cost, mem, stack, contract, evm.env.Depth(), err)