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.

switchcost_test.gno

4.25 Kb · 227 lines
  1package riscv
  2
  3import "testing"
  4
  5// Does the GnoVM dispatch a switch by jump table or by scanning the cases in
  6// order? The answer decides the interpreter's shape: predecoding an image into
  7// (op, rd, rs1, rs2, imm) arrays deletes the fetch and the field extraction,
  8// but it also flattens two small nested switches into one 48-case switch. If
  9// cases are scanned, that trade buys nothing and costs the deeper arms.
 10//
 11// The experiment: the same 48-case switch, driven to the first case and to the
 12// last. Equal cycles means a jump table. A large gap means a scan, and the
 13// switch has to stay shallow no matter what else changes.
 14
 15//go:noinline
 16func switch48(v uint32) uint32 {
 17	switch v {
 18	case 0:
 19		return 1
 20	case 1:
 21		return 2
 22	case 2:
 23		return 3
 24	case 3:
 25		return 4
 26	case 4:
 27		return 5
 28	case 5:
 29		return 6
 30	case 6:
 31		return 7
 32	case 7:
 33		return 8
 34	case 8:
 35		return 9
 36	case 9:
 37		return 10
 38	case 10:
 39		return 11
 40	case 11:
 41		return 12
 42	case 12:
 43		return 13
 44	case 13:
 45		return 14
 46	case 14:
 47		return 15
 48	case 15:
 49		return 16
 50	case 16:
 51		return 17
 52	case 17:
 53		return 18
 54	case 18:
 55		return 19
 56	case 19:
 57		return 20
 58	case 20:
 59		return 21
 60	case 21:
 61		return 22
 62	case 22:
 63		return 23
 64	case 23:
 65		return 24
 66	case 24:
 67		return 25
 68	case 25:
 69		return 26
 70	case 26:
 71		return 27
 72	case 27:
 73		return 28
 74	case 28:
 75		return 29
 76	case 29:
 77		return 30
 78	case 30:
 79		return 31
 80	case 31:
 81		return 32
 82	case 32:
 83		return 33
 84	case 33:
 85		return 34
 86	case 34:
 87		return 35
 88	case 35:
 89		return 36
 90	case 36:
 91		return 37
 92	case 37:
 93		return 38
 94	case 38:
 95		return 39
 96	case 39:
 97		return 40
 98	case 40:
 99		return 41
100	case 41:
101		return 42
102	case 42:
103		return 43
104	case 43:
105		return 44
106	case 44:
107		return 45
108	case 45:
109		return 46
110	case 46:
111		return 47
112	case 47:
113		return 48
114	}
115	return 0
116}
117
118const switchIters = 200000
119
120func TestSwitchCostFirstCase(t *testing.T) {
121	var acc uint32
122	for i := 0; i < switchIters; i++ {
123		acc += switch48(0)
124	}
125	if acc == 0 {
126		t.Fatal("unreachable")
127	}
128}
129
130func TestSwitchCostLastCase(t *testing.T) {
131	var acc uint32
132	for i := 0; i < switchIters; i++ {
133		acc += switch48(47)
134	}
135	if acc == 0 {
136		t.Fatal("unreachable")
137	}
138}
139
140// The control: the same loop with no switch at all, so the two above can be
141// read net of loop and call overhead.
142//
143//go:noinline
144func switchNone(v uint32) uint32 { return v + 1 }
145
146func TestSwitchCostNoSwitch(t *testing.T) {
147	var acc uint32
148	for i := 0; i < switchIters; i++ {
149		acc += switchNone(0)
150	}
151	if acc == 0 {
152		t.Fatal("unreachable")
153	}
154}
155
156// A scan means the question is no longer "how few cases" but "is there a
157// dispatch that does not scan at all". These three isolate the alternatives:
158// the bare loop, a direct call, and a call through a table indexed by opcode.
159
160//go:noinline
161func hADDI(v uint32) uint32 { return v + 1 }
162
163//go:noinline
164func hSUB(v uint32) uint32 { return v - 1 }
165
166//go:noinline
167func hXOR(v uint32) uint32 { return v ^ 1 }
168
169//go:noinline
170func hAND(v uint32) uint32 { return v & 1 }
171
172var dispatchTable = [4]func(uint32) uint32{hADDI, hSUB, hXOR, hAND}
173
174func TestDispatchLoopOnly(t *testing.T) {
175	var acc uint32
176	for i := 0; i < switchIters; i++ {
177		acc += uint32(i) & 1
178	}
179	if acc == 0 {
180		t.Fatal("unreachable")
181	}
182}
183
184func TestDispatchTableCall(t *testing.T) {
185	var acc uint32
186	for i := 0; i < switchIters; i++ {
187		acc += dispatchTable[3](1)
188	}
189	if acc == 0 {
190		t.Fatal("unreachable")
191	}
192}
193
194// What does memory cost? Predecode allocates five arrays per load and the
195// address space allocates a megabyte, so the answer decides whether the
196// predecode arrays should be built eagerly or only for words actually reached.
197
198func TestAllocBytes1000(t *testing.T) {
199	b := make([]byte, 4000)
200	if len(b) != 4000 {
201		t.Fatal("unreachable")
202	}
203}
204
205func TestAllocBytes4000(t *testing.T) {
206	b := make([]byte, 16000)
207	if len(b) != 16000 {
208		t.Fatal("unreachable")
209	}
210}
211
212// The five arrays predecode builds, at the same two sizes.
213func TestAllocArrays1000(t *testing.T) {
214	a, b, c := make([]uint8, 1000), make([]uint8, 1000), make([]uint8, 1000)
215	d, e := make([]uint8, 1000), make([]uint32, 1000)
216	if len(a)+len(b)+len(c)+len(d)+len(e) == 0 {
217		t.Fatal("unreachable")
218	}
219}
220
221func TestAllocArrays4000(t *testing.T) {
222	a, b, c := make([]uint8, 4000), make([]uint8, 4000), make([]uint8, 4000)
223	d, e := make([]uint8, 4000), make([]uint32, 4000)
224	if len(a)+len(b)+len(c)+len(d)+len(e) == 0 {
225		t.Fatal("unreachable")
226	}
227}