Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

micro_test.gno

5.75 Kb · 261 lines
  1package bf
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/x/vm/vmkit/v0"
  7)
  8
  9// The microarchitecture matrix.
 10//
 11// The rungs in ladder_test.gno are the classic interpreter optimizations:
 12// fewer instructions for the same program. These are the other axis, and on
 13// the GnoVM they matter as much: the same instruction stream, executed by
 14// loops that differ from each other by exactly one line.
 15//
 16// They are deliberately simplified (no host, no snapshot, no scan) so that
 17// the one line is the only difference. They are comparable to each other and
 18// not to the shipped machine, which does more. What the shipped machine took
 19// from them is documented on [Machine.Step].
 20
 21type vm struct {
 22	prog *Program
 23	tape [TapeSize]byte
 24	ptr  int
 25	pc   int
 26}
 27
 28// V0: cursors as struct fields. No meter, no bounds check, no defer.
 29func (m *vm) runFields() int64 {
 30	ops := m.prog.ops
 31	var used int64
 32	for {
 33		used++
 34		switch ops[m.pc].code {
 35		case opAdd:
 36			m.tape[m.ptr] += byte(ops[m.pc].arg)
 37		case opMove:
 38			m.ptr = wrap(m.ptr + ops[m.pc].arg)
 39		case opSet:
 40			m.tape[m.ptr] = byte(ops[m.pc].arg)
 41		case opAddMul:
 42			if v := m.tape[m.ptr]; v != 0 {
 43				m.tape[wrap(m.ptr+ops[m.pc].off)] += v * byte(ops[m.pc].arg)
 44			}
 45		case opJmpZ:
 46			if m.tape[m.ptr] == 0 {
 47				m.pc = ops[m.pc].arg
 48			}
 49		case opJmpNZ:
 50			if m.tape[m.ptr] != 0 {
 51				m.pc = ops[m.pc].arg
 52			}
 53		case opHalt:
 54			return used
 55		}
 56		m.pc++
 57	}
 58}
 59
 60// V1: V0 with the two cursors hoisted into locals. Only change.
 61func (m *vm) runLocals() int64 {
 62	ops := m.prog.ops
 63	pc, ptr := m.pc, m.ptr
 64	var used int64
 65	for {
 66		used++
 67		switch ops[pc].code {
 68		case opAdd:
 69			m.tape[ptr] += byte(ops[pc].arg)
 70		case opMove:
 71			ptr = wrap(ptr + ops[pc].arg)
 72		case opSet:
 73			m.tape[ptr] = byte(ops[pc].arg)
 74		case opAddMul:
 75			if v := m.tape[ptr]; v != 0 {
 76				m.tape[wrap(ptr+ops[pc].off)] += v * byte(ops[pc].arg)
 77			}
 78		case opJmpZ:
 79			if m.tape[ptr] == 0 {
 80				pc = ops[pc].arg
 81			}
 82		case opJmpNZ:
 83			if m.tape[ptr] != 0 {
 84				pc = ops[pc].arg
 85			}
 86		case opHalt:
 87			m.pc, m.ptr = pc, ptr
 88			return used
 89		}
 90		pc++
 91	}
 92}
 93
 94// V2: V1 plus a vmkit.Meter charged once per op. Only change.
 95func (m *vm) runLocalsMeter(fuel int64) int64 {
 96	ops := m.prog.ops
 97	meter := vmkit.NewMeter(fuel)
 98	pc, ptr := m.pc, m.ptr
 99	for {
100		if !meter.Charge(1) {
101			m.pc, m.ptr = pc, ptr
102			return meter.Used()
103		}
104		switch ops[pc].code {
105		case opAdd:
106			m.tape[ptr] += byte(ops[pc].arg)
107		case opMove:
108			ptr = wrap(ptr + ops[pc].arg)
109		case opSet:
110			m.tape[ptr] = byte(ops[pc].arg)
111		case opAddMul:
112			if v := m.tape[ptr]; v != 0 {
113				m.tape[wrap(ptr+ops[pc].off)] += v * byte(ops[pc].arg)
114			}
115		case opJmpZ:
116			if m.tape[ptr] == 0 {
117				pc = ops[pc].arg
118			}
119		case opJmpNZ:
120			if m.tape[ptr] != 0 {
121				pc = ops[pc].arg
122			}
123		case opHalt:
124			m.pc, m.ptr = pc, ptr
125			return meter.Used()
126		}
127		pc++
128	}
129}
130
131// V3: V1 plus an inline fuel counter instead of the Meter object. Isolates
132// the cost of the method call from the cost of metering at all.
133func (m *vm) runLocalsInlineFuel(fuel int64) int64 {
134	ops := m.prog.ops
135	pc, ptr := m.pc, m.ptr
136	var used int64
137	for {
138		if used >= fuel {
139			m.pc, m.ptr = pc, ptr
140			return used
141		}
142		used++
143		switch ops[pc].code {
144		case opAdd:
145			m.tape[ptr] += byte(ops[pc].arg)
146		case opMove:
147			ptr = wrap(ptr + ops[pc].arg)
148		case opSet:
149			m.tape[ptr] = byte(ops[pc].arg)
150		case opAddMul:
151			if v := m.tape[ptr]; v != 0 {
152				m.tape[wrap(ptr+ops[pc].off)] += v * byte(ops[pc].arg)
153			}
154		case opJmpZ:
155			if m.tape[ptr] == 0 {
156				pc = ops[pc].arg
157			}
158		case opJmpNZ:
159			if m.tape[ptr] != 0 {
160				pc = ops[pc].arg
161			}
162		case opHalt:
163			m.pc, m.ptr = pc, ptr
164			return used
165		}
166		pc++
167	}
168}
169
170
171// V4: V3 with the tape as a local slice instead of an array field. Only
172// change, so this settles the "a fixed array removes a bounds check" claim.
173func (m *vm) runLocalsInlineFuelSliceTape(fuel int64) int64 {
174	ops := m.prog.ops
175	tape := make([]byte, TapeSize)
176	pc, ptr := m.pc, m.ptr
177	var used int64
178	for {
179		if used >= fuel {
180			m.pc, m.ptr = pc, ptr
181			return used
182		}
183		used++
184		switch ops[pc].code {
185		case opAdd:
186			tape[ptr] += byte(ops[pc].arg)
187		case opMove:
188			ptr = wrap(ptr + ops[pc].arg)
189		case opSet:
190			tape[ptr] = byte(ops[pc].arg)
191		case opAddMul:
192			if v := tape[ptr]; v != 0 {
193				tape[wrap(ptr+ops[pc].off)] += v * byte(ops[pc].arg)
194			}
195		case opJmpZ:
196			if tape[ptr] == 0 {
197				pc = ops[pc].arg
198			}
199		case opJmpNZ:
200			if tape[ptr] != 0 {
201				pc = ops[pc].arg
202			}
203		case opHalt:
204			m.pc, m.ptr = pc, ptr
205			return used
206		}
207		pc++
208	}
209}
210
211func microProg(lvl Level) *Program {
212	p, err := Compile(heavy, lvl)
213	if err != nil {
214		panic(err)
215	}
216	return p
217}
218
219// Rung 1's instruction stream, 121,202 ops, under each loop.
220
221func TestMicroJumpsCursorsAsFields(t *testing.T) {
222	(&vm{prog: microProg(LevelJumps)}).runFields()
223}
224
225func TestMicroJumpsCursorsInLocals(t *testing.T) {
226	(&vm{prog: microProg(LevelJumps)}).runLocals()
227}
228
229func TestMicroJumpsMeterPerOp(t *testing.T) {
230	(&vm{prog: microProg(LevelJumps)}).runLocalsMeter(vmkit.Unmetered)
231}
232
233func TestMicroJumpsInlineFuel(t *testing.T) {
234	(&vm{prog: microProg(LevelJumps)}).runLocalsInlineFuel(1 << 40)
235}
236
237// Rung 3's instruction stream, 1,203 ops, under the same loops.
238
239func TestMicroJumpsSliceTape(t *testing.T) {
240	(&vm{prog: microProg(LevelJumps)}).runLocalsInlineFuelSliceTape(1 << 40)
241}
242
243func TestMicroIdiomsCursorsAsFields(t *testing.T) {
244	(&vm{prog: microProg(LevelIdioms)}).runFields()
245}
246
247func TestMicroIdiomsCursorsInLocals(t *testing.T) {
248	(&vm{prog: microProg(LevelIdioms)}).runLocals()
249}
250
251func TestMicroIdiomsMeterPerOp(t *testing.T) {
252	(&vm{prog: microProg(LevelIdioms)}).runLocalsMeter(vmkit.Unmetered)
253}
254
255func TestMicroIdiomsInlineFuel(t *testing.T) {
256	(&vm{prog: microProg(LevelIdioms)}).runLocalsInlineFuel(1 << 40)
257}
258
259func TestMicroIdiomsSliceTape(t *testing.T) {
260	(&vm{prog: microProg(LevelIdioms)}).runLocalsInlineFuelSliceTape(1 << 40)
261}