core/vm: added JIT segmenting / optimisations

* multi-push segments
* static jumps segments
This commit is contained in:
Jeffrey Wilcke 2015-10-06 23:39:43 +02:00
parent 9d61d78de6
commit b196278044
9 changed files with 205 additions and 5 deletions

View File

@ -475,6 +475,9 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config {
cfg.TestNet = true cfg.TestNet = true
} }
if ctx.GlobalBool(VMEnableJitFlag.Name) {
cfg.Name += "/JIT"
}
if ctx.GlobalBool(DevModeFlag.Name) { if ctx.GlobalBool(DevModeFlag.Name) {
if !ctx.GlobalIsSet(VMDebugFlag.Name) { if !ctx.GlobalIsSet(VMDebugFlag.Name) {
cfg.VmDebug = true cfg.VmDebug = true

View File

@ -24,9 +24,12 @@ invokes the JIT VM in a seperate goroutine and compiles the byte code in JIT
instructions. instructions.
The JIT VM, when invoked, loops around a set of pre-defined instructions until The JIT VM, when invoked, loops around a set of pre-defined instructions until
it either runs of gas, causes an internal error, returns or stops. At a later it either runs of gas, causes an internal error, returns or stops.
stage the JIT VM will see some additional features that will cause sets of
instructions to be compiled down to segments. Segments are sets of instructions The JIT optimiser attempts to pre-compile instructions in to chunks or segments
that can be run in one go saving precious time during execution. such as multiple PUSH operations and static JUMPs. It does this by analysing the
opcodes and attempts to match certain regions to known sets. Whenever the
optimiser finds said segments it creates a new instruction and replaces the
first occurrence in the sequence.
*/ */
package vm package vm

View File

@ -73,7 +73,7 @@ func (instr instruction) do(program *Program, pc *uint64, env Environment, contr
// Resize the memory calculated previously // Resize the memory calculated previously
memory.Resize(newMemSize.Uint64()) memory.Resize(newMemSize.Uint64())
// These opcodes return an argument and are thefor handled // These opcodes return an argument and are therefor handled
// differently from the rest of the opcodes // differently from the rest of the opcodes
switch instr.op { switch instr.op {
case JUMP: case JUMP:

View File

@ -290,6 +290,8 @@ func CompileProgram(program *Program) (err error) {
} }
} }
optimiseProgram(program)
return nil return nil
} }

90
core/vm/jit_optimiser.go Normal file
View File

@ -0,0 +1,90 @@
package vm
import (
"math/big"
"time"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
)
// 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
statsPush = 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\n", program.Id[:4], time.Since(tstart), statsJump, statsPush)
}()
}
for i := 0; i < len(program.instructions); i++ {
instr := program.instructions[i].(instruction)
switch {
case instr.op.IsPush():
load = append(load, instr)
case instr.op.IsStaticJump():
if len(load) == 0 {
continue
}
// if the push load is greater than 1, finalise that
// segment first
if len(load) > 2 {
seg, size := makePushSeg(load[:len(load)-1])
program.instructions[i-size-1] = seg
statsPush++
}
// create a segment consisting of a pre determined
// jump, destination and validity.
seg := makeStaticJumpSeg(load[len(load)-1].data, program)
program.instructions[i-1] = seg
statsJump++
load = nil
default:
// create a new N pushes segment
if len(load) > 1 {
seg, size := makePushSeg(load)
program.instructions[i-size] = seg
statsPush++
}
load = nil
}
}
}
// makePushSeg creates a new push segment from N amount of push instructions
func makePushSeg(instrs []instruction) (pushSeg, int) {
var (
data []*big.Int
gas = new(big.Int)
)
for _, instr := range instrs {
data = append(data, instr.data)
gas.Add(gas, instr.gas)
}
return pushSeg{data, gas}, len(instrs)
}
// makeStaticJumpSeg creates a new static jump segment from a predefined
// destination (PUSH, JUMP).
func makeStaticJumpSeg(to *big.Int, program *Program) jumpSeg {
gas := new(big.Int)
gas.Add(gas, _baseCheck[PUSH1].gas)
gas.Add(gas, _baseCheck[JUMP].gas)
contract := &Contract{Code: program.code}
pos, err := jump(program.mapping, program.destinations, contract, to)
return jumpSeg{pos, err, gas}
}

View File

@ -26,6 +26,49 @@ import (
const maxRun = 1000 const maxRun = 1000
func TestSegmenting(t *testing.T) {
prog := NewProgram([]byte{byte(PUSH1), 0x1, byte(PUSH1), 0x1, 0x0})
err := CompileProgram(prog)
if err != nil {
t.Fatal(err)
}
if instr, ok := prog.instructions[0].(pushSeg); ok {
if len(instr.data) != 2 {
t.Error("expected 2 element width pushSegment, got", len(instr.data))
}
} else {
t.Errorf("expected instr[0] to be a pushSeg, got %T", prog.instructions[0])
}
prog = NewProgram([]byte{byte(PUSH1), 0x1, byte(PUSH1), 0x1, byte(JUMP)})
err = CompileProgram(prog)
if err != nil {
t.Fatal(err)
}
if _, ok := prog.instructions[1].(jumpSeg); ok {
} else {
t.Errorf("expected instr[1] to be jumpSeg, got %T", prog.instructions[1])
}
prog = NewProgram([]byte{byte(PUSH1), 0x1, byte(PUSH1), 0x1, byte(PUSH1), 0x1, byte(JUMP)})
err = CompileProgram(prog)
if err != nil {
t.Fatal(err)
}
if instr, ok := prog.instructions[0].(pushSeg); ok {
if len(instr.data) != 2 {
t.Error("expected 2 element width pushSegment, got", len(instr.data))
}
} else {
t.Errorf("expected instr[0] to be a pushSeg, got %T", prog.instructions[0])
}
if _, ok := prog.instructions[2].(jumpSeg); ok {
} else {
t.Errorf("expected instr[1] to be jumpSeg, got %T", prog.instructions[1])
}
}
func TestCompiling(t *testing.T) { func TestCompiling(t *testing.T) {
prog := NewProgram([]byte{0x60, 0x10}) prog := NewProgram([]byte{0x60, 0x10})
err := CompileProgram(prog) err := CompileProgram(prog)

View File

@ -23,6 +23,18 @@ import (
// OpCode is an EVM opcode // OpCode is an EVM opcode
type OpCode byte type OpCode byte
func (op OpCode) IsPush() bool {
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:
return true
}
return false
}
func (op OpCode) IsStaticJump() bool {
return op == JUMP
}
const ( const (
// 0x0 range - arithmetic ops // 0x0 range - arithmetic ops
STOP OpCode = iota STOP OpCode = iota

44
core/vm/segments.go Normal file
View File

@ -0,0 +1,44 @@
package vm
import "math/big"
type jumpSeg struct {
pos uint64
err error
gas *big.Int
}
func (j jumpSeg) do(program *Program, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack) ([]byte, error) {
if !contract.UseGas(j.gas) {
return nil, OutOfGasError
}
if j.err != nil {
return nil, j.err
}
*pc = j.pos
return nil, nil
}
func (s jumpSeg) halts() bool { return false }
func (s jumpSeg) Op() OpCode { return 0 }
type pushSeg struct {
data []*big.Int
gas *big.Int
}
func (s pushSeg) 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(s.gas) {
return nil, OutOfGasError
}
for _, d := range s.data {
stack.push(new(big.Int).Set(d))
}
*pc += uint64(len(s.data))
return nil, nil
}
func (s pushSeg) halts() bool { return false }
func (s pushSeg) Op() OpCode { return 0 }

View File

@ -42,6 +42,9 @@ func (st *stack) push(d *big.Int) {
//st.data = append(st.data, stackItem) //st.data = append(st.data, stackItem)
st.data = append(st.data, d) st.data = append(st.data, d)
} }
func (st *stack) pushN(ds ...*big.Int) {
st.data = append(st.data, ds...)
}
func (st *stack) pop() (ret *big.Int) { func (st *stack) pop() (ret *big.Int) {
ret = st.data[len(st.data)-1] ret = st.data[len(st.data)-1]