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

bench_test.gno

4.34 Kb · 129 lines
  1package riscv
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/x/vm/vmkit/v0"
  7	"gno.land/p/nt/uassert/v0"
  8)
  9
 10// The measurement the design issue set a kill criterion against: if an RV32I
 11// instruction costs more than roughly 15k cycles after the bf ladder's tricks,
 12// a RISC-V guest runs ~200k instructions per block, which is a token transfer
 13// and not a program a Rust programmer would write without thinking about it.
 14//
 15// The loop is arithmetic and branches only, no syscalls and no memory beyond
 16// the fetch, so it measures the decode-and-dispatch path rather than the host.
 17
 18// li loads a full 32-bit constant, because addi cannot: its immediate is 12
 19// bits SIGN EXTENDED, so addi(rd, x0, 4000) loads -96 and a loop counting up to
 20// it never terminates. That is not hypothetical, it hung this benchmark for two
 21// days. The +0x800 pre-bias is the standard correction: it cancels the sign
 22// extension addi is about to apply to the low half.
 23func li(rd, v uint32) []uint32 {
 24	return []uint32{
 25		lui(rd, (v+0x800)&0xFFFFF000),
 26		addi(rd, rd, v&0xFFF),
 27	}
 28}
 29
 30func lui(rd, imm uint32) uint32 { return uType(imm, rd, opLUI) }
 31
 32// benchProg counts x6 up to x8 in a 4-instruction loop, then exits.
 33func benchProg(n uint32) []uint32 {
 34	prog := li(8, n) // x8 = n, the loop bound
 35	prog = append(prog,
 36		addi(5, 0, 0), // sum = 0
 37		addi(6, 0, 0), // i   = 0
 38		// loop:
 39		add(5, 5, 6),      // sum += i
 40		addi(6, 6, 1),     // i++
 41		addi(7, 0, 0),     // filler, keeps the loop a round 4 instructions
 42		bne(6, 8, 0x1FF4), // -12, while i != x8
 43	)
 44	return append(prog, exitWith()...)
 45}
 46
 47// benchInstrs is how many instructions benchProg(n) executes: 4 setup (li is
 48// two), 4 per iteration including the branch, 2 to exit.
 49func benchInstrs(n int64) int64 { return 4 + 4*n + 2 }
 50
 51// runBench returns the instruction count the machine charged itself. The fuel
 52// cap is ten times what the program should need: a mis-encoded immediate turns
 53// an unmetered run into an infinite loop with no test output, so the budget is
 54// what makes a bad encoding fail in a second instead of hanging the suite.
 55func runBench(t *testing.T, n uint32) int64 {
 56	t.Helper()
 57	m, err := NewMachine(asm(benchProg(n)), entry)
 58	uassert.NoError(t, err)
 59	if m == nil {
 60		return 0
 61	}
 62	used, status := m.Step(vmkit.NewTestHost(), benchInstrs(int64(n))*10)
 63	uassert.Equal(t, "halted", status.String())
 64	return used
 65}
 66
 67// One test per size, so `gno test -print-runtime-metrics` prints one cycle
 68// count each and the slope cancels the fixed per-test overhead. The README
 69// quotes the result.
 70
 71func TestBenchRV32I1000(t *testing.T) {
 72	uassert.Equal(t, benchInstrs(1000), runBench(t, 1000))
 73}
 74
 75func TestBenchRV32I4000(t *testing.T) {
 76	uassert.Equal(t, benchInstrs(4000), runBench(t, 4000))
 77}
 78
 79// li has to survive the sign-extension boundary in both directions, or the two
 80// sizes above are not measuring the loop counts they claim to.
 81func TestLoadImmediateSpansTheSignBoundary(t *testing.T) {
 82	for _, v := range []uint32{0, 1, 0x7FF, 0x800, 1000, 4000, 0x7FFFFFFF, 0xFFFFFFFF} {
 83		prog := append(li(5, v), exitWith()...)
 84		m, err := NewMachine(asm(prog), entry)
 85		uassert.NoError(t, err)
 86		if m == nil {
 87			continue
 88		}
 89		_, status := m.Step(vmkit.NewTestHost(), vmkit.Unmetered)
 90		uassert.Equal(t, "halted", status.String())
 91		uassert.Equal(t, uint64(v), uint64(m.reg[5]))
 92	}
 93}
 94
 95// Predecoding is paid once per load, and a realm loads a machine on every
 96// transaction that resumes one. So the saving on the dispatch loop is only
 97// real if this side of the trade is small against the instructions the slice
 98// then executes. Two sizes again, so the slope is per word.
 99func benchImage(words int) []byte {
100	img := make([]byte, words*4)
101	inst := addi(5, 5, 1)
102	for i := 0; i < words; i++ {
103		img[i*4] = byte(inst)
104		img[i*4+1] = byte(inst >> 8)
105		img[i*4+2] = byte(inst >> 16)
106		img[i*4+3] = byte(inst >> 24)
107	}
108	return img
109}
110
111func TestBenchPredecode1000(t *testing.T) {
112	c := predecode(entry, benchImage(1000))
113	uassert.Equal(t, uint64(1000), uint64(c.words))
114}
115
116func TestBenchPredecode4000(t *testing.T) {
117	c := predecode(entry, benchImage(4000))
118	uassert.Equal(t, uint64(4000), uint64(c.words))
119}
120
121// The control, so the slope above is predecode and not the loop that builds
122// the image.
123func TestBenchImageOnly1000(t *testing.T) {
124	uassert.Equal(t, 4000, len(benchImage(1000)))
125}
126
127func TestBenchImageOnly4000(t *testing.T) {
128	uassert.Equal(t, 16000, len(benchImage(4000)))
129}