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}