Skip to content

Commit 58d0752

Browse files
committed
Merge pull request #1883 from obscuren/jit-vm-optimisations
core/vm: JIT segmentation
2 parents 0467a6c + 8c85532 commit 58d0752

File tree

11 files changed

+472
-83
lines changed

11 files changed

+472
-83
lines changed

cmd/utils/flags.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,9 @@ func MakeEthConfig(clientID, version string, ctx *cli.Context) *eth.Config {
475475
cfg.TestNet = true
476476
}
477477

478+
if ctx.GlobalBool(VMEnableJitFlag.Name) {
479+
cfg.Name += "/JIT"
480+
}
478481
if ctx.GlobalBool(DevModeFlag.Name) {
479482
if !ctx.GlobalIsSet(VMDebugFlag.Name) {
480483
cfg.VmDebug = true

core/vm/doc.go

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

core/vm/instructions.go

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package vm
1818

1919
import (
20+
"fmt"
2021
"math/big"
2122

2223
"github.com/ethereum/go-ethereum/common"
@@ -25,16 +26,16 @@ import (
2526
)
2627

2728
type programInstruction interface {
28-
Do(instr instruction, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack)
29+
// executes the program instruction and allows the instruction to modify the state of the program
30+
do(program *Program, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack) ([]byte, error)
31+
// returns whether the program instruction halts the execution of the JIT
32+
halts() bool
33+
// Returns the current op code (debugging purposes)
34+
Op() OpCode
2935
}
3036

3137
type instrFn func(instr instruction, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack)
3238

33-
// Do executes the function. This implements programInstruction
34-
func (fn instrFn) Do(instr instruction, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack) {
35-
fn(instr, pc, env, contract, memory, stack)
36-
}
37-
3839
type instruction struct {
3940
op OpCode
4041
pc uint64
@@ -44,6 +45,73 @@ type instruction struct {
4445
gas *big.Int
4546
spop int
4647
spush int
48+
49+
returns bool
50+
}
51+
52+
func jump(mapping map[uint64]uint64, destinations map[uint64]struct{}, contract *Contract, to *big.Int) (uint64, error) {
53+
if !validDest(destinations, to) {
54+
nop := contract.GetOp(to.Uint64())
55+
return 0, fmt.Errorf("invalid jump destination (%v) %v", nop, to)
56+
}
57+
58+
return mapping[to.Uint64()], nil
59+
}
60+
61+
func (instr instruction) do(program *Program, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack) ([]byte, error) {
62+
// calculate the new memory size and gas price for the current executing opcode
63+
newMemSize, cost, err := jitCalculateGasAndSize(env, contract, instr, env.Db(), memory, stack)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
// Use the calculated gas. When insufficient gas is present, use all gas and return an
69+
// Out Of Gas error
70+
if !contract.UseGas(cost) {
71+
return nil, OutOfGasError
72+
}
73+
// Resize the memory calculated previously
74+
memory.Resize(newMemSize.Uint64())
75+
76+
// These opcodes return an argument and are therefor handled
77+
// differently from the rest of the opcodes
78+
switch instr.op {
79+
case JUMP:
80+
if pos, err := jump(program.mapping, program.destinations, contract, stack.pop()); err != nil {
81+
return nil, err
82+
} else {
83+
*pc = pos
84+
return nil, nil
85+
}
86+
case JUMPI:
87+
pos, cond := stack.pop(), stack.pop()
88+
if cond.Cmp(common.BigTrue) >= 0 {
89+
if pos, err := jump(program.mapping, program.destinations, contract, pos); err != nil {
90+
return nil, err
91+
} else {
92+
*pc = pos
93+
return nil, nil
94+
}
95+
}
96+
case RETURN:
97+
offset, size := stack.pop(), stack.pop()
98+
return memory.GetPtr(offset.Int64(), size.Int64()), nil
99+
default:
100+
if instr.fn == nil {
101+
return nil, fmt.Errorf("Invalid opcode 0x%x", instr.op)
102+
}
103+
instr.fn(instr, pc, env, contract, memory, stack)
104+
}
105+
*pc++
106+
return nil, nil
107+
}
108+
109+
func (instr instruction) halts() bool {
110+
return instr.returns
111+
}
112+
113+
func (instr instruction) Op() OpCode {
114+
return instr.op
47115
}
48116

49117
func opStaticJump(instr instruction, pc *uint64, ret *big.Int, env Environment, contract *Contract, memory *Memory, stack *stack) {
@@ -536,8 +604,6 @@ func opStop(instr instruction, pc *uint64, env Environment, contract *Contract,
536604
}
537605

538606
func opSuicide(instr instruction, pc *uint64, env Environment, contract *Contract, memory *Memory, stack *stack) {
539-
//receiver := env.Db().GetOrNewStateObject(common.BigToAddress(stack.pop()))
540-
//receiver.AddBalance(balance)
541607
balance := env.Db().GetBalance(contract.Address())
542608
env.Db().AddBalance(common.BigToAddress(stack.pop()), balance)
543609

core/vm/jit.go

Lines changed: 16 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ type Program struct {
8686

8787
contract *Contract
8888

89-
instructions []instruction // instruction set
90-
mapping map[uint64]int // real PC mapping to array indices
91-
destinations map[uint64]struct{} // cached jump destinations
89+
instructions []programInstruction // instruction set
90+
mapping map[uint64]uint64 // real PC mapping to array indices
91+
destinations map[uint64]struct{} // cached jump destinations
9292

9393
code []byte
9494
}
@@ -97,7 +97,7 @@ type Program struct {
9797
func NewProgram(code []byte) *Program {
9898
program := &Program{
9999
Id: crypto.Sha3Hash(code),
100-
mapping: make(map[uint64]int),
100+
mapping: make(map[uint64]uint64),
101101
destinations: make(map[uint64]struct{}),
102102
code: code,
103103
}
@@ -118,10 +118,12 @@ func (p *Program) addInstr(op OpCode, pc uint64, fn instrFn, data *big.Int) {
118118
baseOp = DUP1
119119
}
120120
base := _baseCheck[baseOp]
121-
instr := instruction{op, pc, fn, data, base.gas, base.stackPop, base.stackPush}
121+
122+
returns := op == RETURN || op == SUICIDE || op == STOP
123+
instr := instruction{op, pc, fn, data, base.gas, base.stackPop, base.stackPush, returns}
122124

123125
p.instructions = append(p.instructions, instr)
124-
p.mapping[pc] = len(p.instructions) - 1
126+
p.mapping[pc] = uint64(len(p.instructions) - 1)
125127
}
126128

127129
// CompileProgram compiles the given program and return an error when it fails
@@ -288,6 +290,8 @@ func CompileProgram(program *Program) (err error) {
288290
}
289291
}
290292

293+
optimiseProgram(program)
294+
291295
return nil
292296
}
293297

@@ -301,21 +305,8 @@ func runProgram(program *Program, pcstart uint64, mem *Memory, stack *stack, env
301305
contract.Input = input
302306

303307
var (
304-
caller = contract.caller
305-
statedb = env.Db()
306-
pc int = program.mapping[pcstart]
307-
instrCount = 0
308-
309-
jump = func(to *big.Int) error {
310-
if !validDest(program.destinations, to) {
311-
nop := contract.GetOp(to.Uint64())
312-
return fmt.Errorf("invalid jump destination (%v) %v", nop, to)
313-
}
314-
315-
pc = program.mapping[to.Uint64()]
316-
317-
return nil
318-
}
308+
pc uint64 = program.mapping[pcstart]
309+
instrCount = 0
319310
)
320311

321312
if glog.V(logger.Debug) {
@@ -326,62 +317,19 @@ func runProgram(program *Program, pcstart uint64, mem *Memory, stack *stack, env
326317
}()
327318
}
328319

329-
for pc < len(program.instructions) {
320+
for pc < uint64(len(program.instructions)) {
330321
instrCount++
331322

332323
instr := program.instructions[pc]
333324

334-
// calculate the new memory size and gas price for the current executing opcode
335-
newMemSize, cost, err := jitCalculateGasAndSize(env, contract, caller, instr, statedb, mem, stack)
325+
ret, err := instr.do(program, &pc, env, contract, mem, stack)
336326
if err != nil {
337327
return nil, err
338328
}
339329

340-
// Use the calculated gas. When insufficient gas is present, use all gas and return an
341-
// Out Of Gas error
342-
if !contract.UseGas(cost) {
343-
return nil, OutOfGasError
344-
}
345-
// Resize the memory calculated previously
346-
mem.Resize(newMemSize.Uint64())
347-
348-
// These opcodes return an argument and are thefor handled
349-
// differently from the rest of the opcodes
350-
switch instr.op {
351-
case JUMP:
352-
if err := jump(stack.pop()); err != nil {
353-
return nil, err
354-
}
355-
continue
356-
case JUMPI:
357-
pos, cond := stack.pop(), stack.pop()
358-
359-
if cond.Cmp(common.BigTrue) >= 0 {
360-
if err := jump(pos); err != nil {
361-
return nil, err
362-
}
363-
continue
364-
}
365-
case RETURN:
366-
offset, size := stack.pop(), stack.pop()
367-
ret := mem.GetPtr(offset.Int64(), size.Int64())
368-
330+
if instr.halts() {
369331
return contract.Return(ret), nil
370-
case SUICIDE:
371-
instr.fn(instr, nil, env, contract, mem, stack)
372-
373-
return contract.Return(nil), nil
374-
case STOP:
375-
return contract.Return(nil), nil
376-
default:
377-
if instr.fn == nil {
378-
return nil, fmt.Errorf("Invalid opcode %x", instr.op)
379-
}
380-
381-
instr.fn(instr, nil, env, contract, mem, stack)
382332
}
383-
384-
pc++
385333
}
386334

387335
contract.Input = nil
@@ -403,7 +351,7 @@ func validDest(dests map[uint64]struct{}, dest *big.Int) bool {
403351

404352
// jitCalculateGasAndSize calculates the required given the opcode and stack items calculates the new memorysize for
405353
// the operation. This does not reduce gas or resizes the memory.
406-
func jitCalculateGasAndSize(env Environment, contract *Contract, caller ContractRef, instr instruction, statedb Database, mem *Memory, stack *stack) (*big.Int, *big.Int, error) {
354+
func jitCalculateGasAndSize(env Environment, contract *Contract, instr instruction, statedb Database, mem *Memory, stack *stack) (*big.Int, *big.Int, error) {
407355
var (
408356
gas = new(big.Int)
409357
newMemSize *big.Int = new(big.Int)

core/vm/jit_optimiser.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package vm
2+
3+
import (
4+
"math/big"
5+
"time"
6+
7+
"github.com/ethereum/go-ethereum/logger"
8+
"github.com/ethereum/go-ethereum/logger/glog"
9+
)
10+
11+
// optimeProgram optimises a JIT program creating segments out of program
12+
// instructions. Currently covered are multi-pushes and static jumps
13+
func optimiseProgram(program *Program) {
14+
var load []instruction
15+
16+
var (
17+
statsJump = 0
18+
statsPush = 0
19+
)
20+
21+
if glog.V(logger.Debug) {
22+
glog.Infof("optimising %x\n", program.Id[:4])
23+
tstart := time.Now()
24+
defer func() {
25+
glog.Infof("optimised %x done in %v with JMP: %d PSH: %d\n", program.Id[:4], time.Since(tstart), statsJump, statsPush)
26+
}()
27+
}
28+
29+
/*
30+
code := Parse(program.code)
31+
for _, test := range [][]OpCode{
32+
[]OpCode{PUSH, PUSH, ADD},
33+
[]OpCode{PUSH, PUSH, SUB},
34+
[]OpCode{PUSH, PUSH, MUL},
35+
[]OpCode{PUSH, PUSH, DIV},
36+
} {
37+
matchCount := 0
38+
MatchFn(code, test, func(i int) bool {
39+
matchCount++
40+
return true
41+
})
42+
fmt.Printf("found %d match count on: %v\n", matchCount, test)
43+
}
44+
*/
45+
46+
for i := 0; i < len(program.instructions); i++ {
47+
instr := program.instructions[i].(instruction)
48+
49+
switch {
50+
case instr.op.IsPush():
51+
load = append(load, instr)
52+
case instr.op.IsStaticJump():
53+
if len(load) == 0 {
54+
continue
55+
}
56+
// if the push load is greater than 1, finalise that
57+
// segment first
58+
if len(load) > 2 {
59+
seg, size := makePushSeg(load[:len(load)-1])
60+
program.instructions[i-size-1] = seg
61+
statsPush++
62+
}
63+
// create a segment consisting of a pre determined
64+
// jump, destination and validity.
65+
seg := makeStaticJumpSeg(load[len(load)-1].data, program)
66+
program.instructions[i-1] = seg
67+
statsJump++
68+
69+
load = nil
70+
default:
71+
// create a new N pushes segment
72+
if len(load) > 1 {
73+
seg, size := makePushSeg(load)
74+
program.instructions[i-size] = seg
75+
statsPush++
76+
}
77+
load = nil
78+
}
79+
}
80+
}
81+
82+
// makePushSeg creates a new push segment from N amount of push instructions
83+
func makePushSeg(instrs []instruction) (pushSeg, int) {
84+
var (
85+
data []*big.Int
86+
gas = new(big.Int)
87+
)
88+
89+
for _, instr := range instrs {
90+
data = append(data, instr.data)
91+
gas.Add(gas, instr.gas)
92+
}
93+
94+
return pushSeg{data, gas}, len(instrs)
95+
}
96+
97+
// makeStaticJumpSeg creates a new static jump segment from a predefined
98+
// destination (PUSH, JUMP).
99+
func makeStaticJumpSeg(to *big.Int, program *Program) jumpSeg {
100+
gas := new(big.Int)
101+
gas.Add(gas, _baseCheck[PUSH1].gas)
102+
gas.Add(gas, _baseCheck[JUMP].gas)
103+
104+
contract := &Contract{Code: program.code}
105+
pos, err := jump(program.mapping, program.destinations, contract, to)
106+
return jumpSeg{pos, err, gas}
107+
}

0 commit comments

Comments
 (0)