Skip to content

Commit c42a9b9

Browse files
ggreifclaude
andauthored
bench: add variant-switch.mo (baseline) (#6033)
## Summary Adds `test/bench/variant-switch.mo` — a small GHC-Core-like interpreter that exercises 9-arm variant-switch dispatch in several shapes. Serves as a reference point (baseline on `master`) for the dispatch-optimisation work on #5927 (masked `br_table`) and any follow-ups. Actor methods: - `go` — top-level `size tree + size fibCore`, ×10k. - `evalBench` — `fib(7)` via direct AST eval, compiled finally-tagless form, and the AST→FT transform itself; ×100 each. - `weekdayBench` — `isWeekend` (7 explicit arms) vs `isWeekendOr` (same dispatch via `or`-patterns); ×10k over a 7-arm `Weekday` variant. - `getPerfData` — reports `rts_lifetime_instructions`. ## Master baseline | Metric | Instructions | |---|---| | `size tree + fibCore` (×10k) | 137,590,321 | | `eval fib(7)` AST (×100) | 24,509,348 | | `eval fib(7)` FT (×100) | 21,536,248 | | AST→FT transform (×100) | 1,189,148 | | `isWeekend` (×10k × 7 arms) | 10,010,321 | | `isWeekendOr` (×10k × 7 arms) | 11,050,321 | With #5927 applied, the explicit-arm dispatch numbers drop materially; or-patterns currently don't benefit (separate follow-up). ## Test plan - [x] `make -C test/bench variant-switch.only` passes on `master`. - [x] drun output captured in `test/bench/ok/variant-switch.drun-run.ok`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cff143d commit c42a9b9

2 files changed

Lines changed: 349 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ingress Completed: Reply: 0x4449444c016c01b3c4b1f204680100010a00000000000000000101
2+
ingress Completed: Reply: 0x4449444c0000
3+
debug.print: {heap_diff = 0; instr_diff = 137_590_321; total = 1_090_000}
4+
ingress Completed: Reply: 0x4449444c0000
5+
debug.print: {fib7_eval = 13; fib7_evalFT = 13; fib7_xform = 13; instr_eval = 24_509_348; instr_evalFT = 21_536_248; instr_transform = 1_189_148}
6+
ingress Completed: Reply: 0x4449444c0000
7+
debug.print: {acc1 = 20_000; acc2 = 20_000; instr_isWeekend = 10_010_321; instr_isWeekendOr = 11_050_321}
8+
ingress Completed: Reply: 0x4449444c0000
9+
ingress Completed: Reply: 0x4449444c0000

test/bench/variant-switch.mo

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
// Benchmark: small interpreter for a GHC-Core-like expression language.
2+
// Exercises a 9-arm variant switch (the hot path) heavily.
3+
//
4+
// Constructors:
5+
// Var, Lit, App, Lam, Let, LetRec, Case, Con, Prim
6+
import {
7+
performanceCounter;
8+
rts_heap_size;
9+
debugPrint;
10+
rts_lifetime_instructions;
11+
Array_tabulate;
12+
} = "mo:⛔";
13+
14+
persistent actor Core {
15+
16+
type Expr = {
17+
#Var : Text;
18+
#Lit : Int;
19+
#App : (Expr, Expr);
20+
#Lam : (Text, Expr);
21+
#Let : (Text, Expr, Expr); // name, rhs, body
22+
#LetRec : [(Text, Expr, Expr)]; // list of (name, rhs, body)
23+
#Case : (Expr, [(Text, Expr)]); // scrutinee, alts
24+
#Con : (Text, [Expr]); // constructor name, args
25+
#Prim : Char; // primitive operation
26+
};
27+
28+
// Count all nodes in an expression tree
29+
func size(e : Expr) : Nat =
30+
switch e {
31+
case (#Var _) 1;
32+
case (#Lit _) 1;
33+
case (#App (f, x)) 1 + size f + size x;
34+
case (#Lam (_, b)) 1 + size b;
35+
case (#Let (_, r, b)) 1 + size r + size b;
36+
case (#LetRec triples) 1 + sumTriples triples;
37+
case (#Case(s, alts)) 1 + size s + sumAlts alts;
38+
case (#Con (_, args)) 1 + sumArgs args;
39+
case (#Prim _) 1;
40+
};
41+
42+
func sumTriples(ts : [(Text, Expr, Expr)]) : Nat {
43+
var n = 0;
44+
for ((_, r, b) in ts.vals()) n += size r + size b;
45+
n
46+
};
47+
48+
func sumAlts(alts : [(Text, Expr)]) : Nat {
49+
var n = 0;
50+
for ((_, e) in alts.vals()) n += size e;
51+
n
52+
};
53+
54+
func sumArgs(args : [Expr]) : Nat {
55+
var n = 0;
56+
for (e in args.vals()) n += size e;
57+
n
58+
};
59+
60+
// Build a synthetic expression tree touching all 9 constructors
61+
func build(d : Nat) : Expr {
62+
if (d == 0) return #Lit 0;
63+
let s = build (d - 1 : Nat);
64+
switch (d % 9) {
65+
case 0 #App (#Var "x", s);
66+
case 1 #Lam ("k", s);
67+
case 2 #App (s, #Var "y");
68+
case 3 #Lam ("z", s);
69+
case 4 #Let ("w", s, #Var "w");
70+
case 5 #LetRec ([("f", s, #App (#Var "f", #Lit 0))]);
71+
case 6 #Case (s, [("A", #Lit 1), ("B", s)]);
72+
case 7 #Con ("Pair", [s, #Var "v"]);
73+
case _ #App (#Prim '+', s);
74+
}
75+
};
76+
77+
transient let tree = build 15; // all 9 constructors
78+
79+
// naïve fib in Core (Peano naturals; #Prim '+' = add, #Prim '-' = pred)
80+
// fib 0 = 0
81+
// fib (S 0) = 1
82+
// fib (S n) = fib n + fib (pred n)
83+
transient let fibCore : Expr =
84+
#LetRec ([(
85+
"fib",
86+
#Lam ("n",
87+
#Case (#Var "n", [
88+
("0", #Con ("0", [])),
89+
("+1",
90+
#Case (#App (#Prim '-', #Var "n"), [
91+
("0", #Con ("+1", [#Con ("0", [])])),
92+
("+1",
93+
#Let ("n1", #App (#Prim '-', #Var "n"),
94+
#App (
95+
#App (#Prim '+',
96+
#App (#Var "fib", #Var "n1")),
97+
#App (#Var "fib", #App (#Prim '-', #Var "n1")))))
98+
]))
99+
])),
100+
#Var "fib"
101+
)]);
102+
103+
// ── Weekday: 7-arm variant to compare explicit-arm vs or-pattern dispatch ─
104+
type Weekday = { #Mon; #Tue; #Wed; #Thu; #Fri; #Sat; #Sun };
105+
106+
func isWeekend(d : Weekday) : Bool =
107+
switch d {
108+
case (#Mon) false;
109+
case (#Tue) false;
110+
case (#Wed) false;
111+
case (#Thu) false;
112+
case (#Fri) false;
113+
case (#Sat) true;
114+
case (#Sun) true;
115+
};
116+
117+
func isWeekendOr(d : Weekday) : Bool =
118+
switch d {
119+
case (#Mon or #Tue or #Wed or #Thu or #Fri) false;
120+
case (#Sat or #Sun) true;
121+
};
122+
123+
transient let week : [Weekday] =
124+
[#Mon, #Tue, #Wed, #Thu, #Fri, #Sat, #Sun];
125+
126+
// ── Runtime values ───────────────────────────────────────────────────────
127+
type Val = { #VInt : Int; #VFun : Val -> Val; #VCon : (Text, [Val]) };
128+
type Env = Text -> Val;
129+
130+
transient let emptyEnv : Env = func(_) { assert false; #VInt 0 };
131+
func extend(env : Env, x : Text, v : Val) : Env =
132+
func(y) = if (y == x) v else env y;
133+
func applyVal(f : Val, v : Val) : Val = switch f {
134+
case (#VFun g) g v;
135+
case _ { assert false; #VInt 0 };
136+
};
137+
138+
// Peano helpers
139+
func addPeano(a : Val, b : Val) : Val = switch a {
140+
case (#VCon (tag, args)) switch tag {
141+
case "0" b;
142+
case "+1" #VCon ("+1", [addPeano (args[0], b)]);
143+
case _ { assert false; #VInt 0 };
144+
};
145+
case _ { assert false; #VInt 0 };
146+
};
147+
func predPeano(v : Val) : Val = switch v {
148+
case (#VCon (_, args)) args[0];
149+
case _ { assert false; #VInt 0 };
150+
};
151+
func evalPrimOp(c : Char) : Val = switch c {
152+
case '+' #VFun (func(a) = #VFun (func(b) = addPeano (a, b)));
153+
case '-' #VFun predPeano;
154+
case _ { assert false; #VInt 0 };
155+
};
156+
func peano(n : Nat) : Val {
157+
if (n == 0) #VCon ("0", [])
158+
else #VCon ("+1", [peano (n - 1 : Nat)])
159+
};
160+
func fromPeano(v : Val) : Nat = switch v {
161+
case (#VCon (tag, args)) switch tag {
162+
case "0" 0;
163+
case "+1" 1 + fromPeano (args[0]);
164+
case _ { assert false; 0 };
165+
};
166+
case _ { assert false; 0 };
167+
};
168+
169+
// ── Direct AST interpreter ────────────────────────────────────────────────
170+
func eval(e : Expr, env : Env) : Val = switch e {
171+
case (#Var x) env x;
172+
case (#Lit n) #VInt n;
173+
case (#Prim c) evalPrimOp c;
174+
case (#App (f, x)) applyVal (eval(f, env), eval(x, env));
175+
case (#Lam (x, b)) #VFun (func(v) = eval(b, extend(env, x, v)));
176+
case (#Let (x, r, b)) eval(b, extend(env, x, eval(r, env)));
177+
case (#LetRec triples) {
178+
let (x, rhs, body) = triples[0];
179+
var cell : Val = #VInt 0;
180+
let recEnv = extend(env, x, #VFun (func(v) = applyVal (cell, v)));
181+
cell := eval(rhs, recEnv);
182+
eval(body, recEnv)
183+
};
184+
case (#Case (s, alts)) {
185+
switch (eval(s, env)) {
186+
case (#VCon (tag, _)) {
187+
for ((altTag, altBody) in alts.vals()) {
188+
if (tag == altTag) return eval(altBody, env);
189+
};
190+
assert false; #VInt 0
191+
};
192+
case _ { assert false; #VInt 0 };
193+
}
194+
};
195+
case (#Con (t, args))
196+
#VCon (t, Array_tabulate (args.size(), func(i) = eval(args[i], env)));
197+
};
198+
199+
// ── Finally-tagless interpreter ───────────────────────────────────────────
200+
// FT: a compiled term — just a closure Env -> Val, no more variant dispatch
201+
type FT = Env -> Val;
202+
203+
type Symantics = {
204+
lit : Int -> FT;
205+
var_ : Text -> FT;
206+
app : (FT, FT) -> FT;
207+
lam : (Text, FT) -> FT;
208+
let_ : (Text, FT, FT) -> FT;
209+
letRec : [(Text, FT, FT)] -> FT;
210+
case_ : (FT, [(Text, FT)]) -> FT;
211+
con : (Text, [FT]) -> FT;
212+
prim : Char -> FT;
213+
};
214+
215+
transient let evalSem : Symantics = {
216+
lit = func(n) = func(_) = #VInt n;
217+
var_ = func(x) = func(env) = env x;
218+
app = func(f, x) = func(env) = applyVal (f env, x env);
219+
lam = func(x, b) = func(env) = #VFun (func(v) = b (extend(env, x, v)));
220+
let_ = func(x, r, b) = func(env) = b (extend(env, x, r env));
221+
letRec = func(triples) = func(env) {
222+
let (x, rhs, body) = triples[0];
223+
var cell : Val = #VInt 0;
224+
let recEnv = extend(env, x, #VFun (func(v) = applyVal (cell, v)));
225+
cell := rhs recEnv;
226+
body recEnv
227+
};
228+
case_ = func(scr, alts) = func(env) {
229+
switch (scr env) {
230+
case (#VCon (tag, _)) {
231+
for ((altTag, altBody) in alts.vals()) {
232+
if (tag == altTag) return altBody env;
233+
};
234+
assert false; #VInt 0
235+
};
236+
case _ { assert false; #VInt 0 };
237+
}
238+
};
239+
con = func(t, args) = func(env) =
240+
#VCon (t, Array_tabulate (args.size(), func(i) = args[i] env));
241+
prim = func(c) = func(_) = evalPrimOp c;
242+
};
243+
244+
func transform(sem : Symantics, e : Expr) : FT = switch e {
245+
case (#Var x) sem.var_ x;
246+
case (#Lit n) sem.lit n;
247+
case (#Prim c) sem.prim c;
248+
case (#App (f, x)) sem.app (transform(sem, f), transform(sem, x));
249+
case (#Lam (x, b)) sem.lam (x, transform(sem, b));
250+
case (#Let (x, r, b)) sem.let_ (x, transform(sem, r), transform(sem, b));
251+
case (#LetRec triples) sem.letRec (Array_tabulate (triples.size(), func(i) {
252+
let (x, r, b) = triples[i]; (x, transform(sem, r), transform(sem, b))
253+
}));
254+
case (#Case (s, alts)) sem.case_ (transform(sem, s), Array_tabulate (alts.size(), func(i) {
255+
let (t, e2) = alts[i]; (t, transform(sem, e2))
256+
}));
257+
case (#Con (t, args))
258+
sem.con (t, Array_tabulate (args.size(), func(i) = transform(sem, args[i])));
259+
};
260+
261+
transient let fibFT : FT = transform(evalSem, fibCore);
262+
transient let seven : Val = peano 7; // fib(7) = 13
263+
264+
func counters() : (Int, Nat64) = (rts_heap_size(), performanceCounter(0));
265+
266+
public func go() : async () {
267+
let (m0, n0) = counters();
268+
var total = 0;
269+
var i = 0;
270+
while (i < 10_000) {
271+
total += size tree + size fibCore;
272+
i += 1;
273+
};
274+
let (m1, n1) = counters();
275+
debugPrint(debug_show { total; heap_diff = m1 - m0; instr_diff = n1 - n0 });
276+
};
277+
278+
public func getPerfData() : async () {
279+
debugPrint("instructions: " # debug_show (rts_lifetime_instructions()));
280+
};
281+
282+
// Benchmark: eval fib(7) via direct AST interpreter vs FT (100 iterations each)
283+
// Also benchmarks AST→FT transform itself (pure Expr variant dispatch).
284+
public func evalBench() : async () {
285+
let fibFn : Val = eval(fibCore, emptyEnv); // AST: fib function via eval
286+
let fibFnFT : Val = fibFT emptyEnv; // FT: fib function via compiled form
287+
288+
let (_m0, n0) = counters();
289+
var r : Val = seven;
290+
var i = 0;
291+
while (i < 100) { r := applyVal (fibFn, seven); i += 1 };
292+
let (_m1, n1) = counters();
293+
var r2 : Val = seven;
294+
var j = 0;
295+
while (j < 100) { r2 := applyVal (fibFnFT, seven); j += 1 };
296+
let (_m2, n2) = counters();
297+
var xform : FT = fibFT;
298+
var k = 0;
299+
while (k < 100) { xform := transform(evalSem, fibCore); k += 1 };
300+
let (_m3, n3) = counters();
301+
debugPrint(debug_show {
302+
fib7_eval = fromPeano r;
303+
fib7_evalFT = fromPeano r2;
304+
fib7_xform = fromPeano (applyVal (xform emptyEnv, seven));
305+
instr_eval = n1 - n0;
306+
instr_evalFT = n2 - n1;
307+
instr_transform = n3 - n2;
308+
});
309+
};
310+
// Benchmark: isWeekend vs isWeekendOr over all 7 inputs, 10k iterations each.
311+
// Same dispatch semantics, different source shape — instruction counts should
312+
// match if or-pattern arms compile to the same br_table path.
313+
public func weekdayBench() : async () {
314+
let (_m0, n0) = counters();
315+
var acc1 = 0;
316+
var i = 0;
317+
while (i < 10_000) {
318+
for (d in week.vals()) { if (isWeekend d) acc1 += 1 };
319+
i += 1;
320+
};
321+
let (_m1, n1) = counters();
322+
var acc2 = 0;
323+
var j = 0;
324+
while (j < 10_000) {
325+
for (d in week.vals()) { if (isWeekendOr d) acc2 += 1 };
326+
j += 1;
327+
};
328+
let (_m2, n2) = counters();
329+
debugPrint(debug_show {
330+
acc1; acc2;
331+
instr_isWeekend = n1 - n0;
332+
instr_isWeekendOr = n2 - n1;
333+
});
334+
};
335+
};
336+
337+
//CALL ingress go 0x4449444C0000
338+
//CALL ingress evalBench 0x4449444C0000
339+
//CALL ingress weekdayBench 0x4449444C0000
340+
//CALL ingress getPerfData 0x4449444C0000

0 commit comments

Comments
 (0)