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

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}