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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bench_test.gno

4.35 Kb · 129 lines
  1package riscv
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/nym-moultest123/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}