code wiki / _hdl_build / nx_eqsat_race_bench.nx
nx_eqsat_race_bench.nx source
↩ module page · 151 lines · 7443 B
1// nx_eqsat_race_bench.nx -- RACE TIMING HARNESS vs egg 0.11.0 on the SAME task.
2//
3// THE RACE: egg's /tmp/race_egg/examples/nx_race.rs timed E2
4// "(* x 8)" : parse-string -> RecExpr -> Runner -> saturate ->
5// Extractor::find_best -> to_string == (<< x 3)
6// 100000 iterations = 783465 us total = 7.835 us/op (LatCost model).
7//
8// OUR analogue, ONE timed iteration (the same end-to-end pipeline, no proxy):
9// nx_eqsat_init (fresh e-graph -- egg's RecExpr alloc)
10// add_var x, add_const 8,
11// add_binary(MUL, x, 8) (BUILD the (* x 8) input -- egg's parse)
12// nx_eqsat_saturate(g, 16) (egg's Runner::run to saturation)
13// nx_eqsat_recompute_best(g) (egg's Extractor bottom-up cost pass)
14// nx_eqsat_extract_best_node(mul) (egg's find_best for the mul e-class)
15// read the root op of that node (egg's to_string root-symbol read)
16// CONFIRM the extracted op == NX_EQ_OP_SHL (11) and shift const == 3.
17//
18// Everything that egg counted (parse->saturate->extract->stringify-root) is
19// inside OUR timed loop too, so the us/op is comparable 1:1. We do NOT include
20// the membership PROOF/provenance machinery (egg has none) -- only the same
21// optimize-and-read-the-answer work egg performed.
22//
23// FAIL LOUD: if the extracted representative is not (shl x 3) the harness
24// sys_exit(nonzero) BEFORE printing timing -- a fast wrong answer is not a win.
25//
26// Output (one line): RESULT_OP=11 SHIFT=3 ITERS=100000 TOTAL_US=<t> US_PER_OP=<u>
27
28import "nx_eqsat.nx"
29const K_MAGIC_100000: i64 = 100000
30
31// --- tiny integer printer (space-terminated) -------------------------------
32func _emit_num(v: i64) -> i64 {
33 let b: *u8 = sys_mmap(28)
34 var n: i64 = v
35 if n < 0 { n = 0 - n }
36 let t2: *u8 = sys_mmap(28)
37 var t: i64 = 0
38 if n == 0 { t2[0] = 48; t = 1 }
39 while n > 0 { t2[t] = 48 + (n % 10); n = n / 10; t = t + 1 }
40 var i: i64 = 0
41 while i < t { b[i] = t2[t - 1 - i]; i = i + 1 }
42 b[t] = 32
43 sys_write(1, b, t + 1)
44 return 0
45}
46
47func _emit_str(s: *u8, len: i64) -> i64 {
48 sys_write(1, s, len)
49 return 0
50}
51
52func _nl() -> i64 {
53 let z: *u8 = sys_mmap(2)
54 z[0] = 10
55 sys_write(1, z, 1)
56 return 0
57}
58
59// Run ONE full optimize-and-read iteration on a freshly-initialised e-graph.
60// Returns the root op of the extracted best representative of the mul e-class,
61// or a negative error sentinel. Writes the extracted shift-amount const to
62// shift_out[0] when the result is SHL (so the caller can confirm shift == 3).
63// The caller hands in the (reused) scratch arrays so per-iteration mmap churn
64// (which egg does NOT incur per parse -- it reuses its arena) is not unfairly
65// charged to us; the e-graph is fully re-init'd each call (n_nodes/n_classes
66// reset to 0) so every iteration rebuilds (* x 8) from scratch like egg's
67// per-iteration string parse.
68func _one_iter(eg: *NxEGraph,
69 nodes: *NxENode, cap_nodes: i64,
70 classes: *NxEClass, cap_cls: i64,
71 shift_out: *i64) -> i64 {
72 if nx_eqsat_init(eg, nodes, cap_nodes, classes, cap_cls) != NX_EQSAT_OK { return 0 - 100 }
73 // BUILD (* x 8) -- the analogue of egg parsing "(* x 8)" into a RecExpr.
74 let vx: i64 = nx_eqsat_add_var(eg, 0)
75 let c8: i64 = nx_eqsat_add_const(eg, 8)
76 let mul8: i64 = nx_eqsat_add_binary(eg, NX_EQ_OP_MUL, vx, c8)
77 if mul8 < 0 { return 0 - 101 }
78 // SATURATE -- egg's Runner::run.
79 let sat: i64 = nx_eqsat_saturate(eg, 16)
80 if sat != NX_EQSAT_SATURATED { return 0 - 102 }
81 // EXTRACT -- bottom-up cost recompute then read the mul class's best node.
82 if nx_eqsat_recompute_best(eg) != NX_EQSAT_OK { return 0 - 103 }
83 let best: i64 = nx_eqsat_extract_best_node(eg, mul8)
84 if best < 0 { return 0 - 104 }
85 let root_op: i64 = eg.nodes[best].op
86 // to_string-root analogue: read the root op + (when SHL) the shift const.
87 if root_op == NX_EQ_OP_SHL {
88 let kid1: i64 = eg.nodes[best].kid1 // shift-amount e-class
89 let kcanon: i64 = nx_eqsat_find(eg, kid1)
90 let knode: i64 = eg.classes[kcanon].best_node
91 shift_out[0] = eg.nodes[knode].payload
92 }
93 return root_op
94}
95
96func main() -> i64 {
97 let cap_nodes: i64 = 64
98 let cap_cls: i64 = 64
99 // Allocate scratch ONCE (matched to the proven membership-proof layout:
100 // NxENode 64B stride, NxEClass 32B stride, NxEGraph 96B). Reused across
101 // iterations; the e-graph contents are fully reset by nx_eqsat_init.
102 let nodes: *NxENode = sys_mmap(cap_nodes * 64) as *NxENode
103 let classes: *NxEClass = sys_mmap(cap_cls * 32) as *NxEClass
104 let eg: *NxEGraph = sys_mmap(96) as *NxEGraph
105 let shift_out: *i64 = sys_mmap(8) as *i64
106
107 // ---- CORRECTNESS GATE (FAIL LOUD, before any timing) -------------------
108 // One verified iteration: the extracted best representative of (* x 8) MUST
109 // be (shl x 3) -- root op SHL(11), shift const 3. A fast wrong answer loses.
110 shift_out[0] = 0 - 1
111 let check_op: i64 = _one_iter(eg, nodes, cap_nodes, classes, cap_cls, shift_out)
112 if check_op != NX_EQ_OP_SHL { sys_exit(80); return 80 } // not SHL => LOUD fail
113 if shift_out[0] != 3 { sys_exit(81); return 81 } // wrong shift => LOUD fail
114
115 // ---- TIMED LOOP --------------------------------------------------------
116 // 100000 iterations of {re-init + rebuild (* x 8) + saturate +
117 // recompute_best + extract + read-root}, bracketed by sys_clock_now_us().
118 // Each iteration is independently verified to still extract SHL/3 so a
119 // miscompile or regression mid-run cannot masquerade as a fast time.
120 let iters: i64 = K_MAGIC_100000
121 let t0: i64 = sys_clock_now_us()
122 var k: i64 = 0
123 while k < iters {
124 shift_out[0] = 0 - 1
125 let op: i64 = _one_iter(eg, nodes, cap_nodes, classes, cap_cls, shift_out)
126 if op != NX_EQ_OP_SHL { sys_exit(82); return 82 } // FAIL LOUD inside loop
127 if shift_out[0] != 3 { sys_exit(83); return 83 }
128 k = k + 1
129 }
130 let t1: i64 = sys_clock_now_us()
131 let total_us: i64 = t1 - t0
132
133 // us/op scaled x1000 (integer) so we can print fractional microseconds:
134 // us_per_op_milli = total_us * 1000 / iters (i.e. nanoseconds/op).
135 let us_per_op_milli: i64 = (total_us * 1000) / iters
136
137 // ---- REPORT ------------------------------------------------------------
138 let s_rop: *u8 = sys_mmap(16); s_rop[0]=82; s_rop[1]=69; s_rop[2]=83; s_rop[3]=85; s_rop[4]=76; s_rop[5]=84; s_rop[6]=95; s_rop[7]=79; s_rop[8]=80; s_rop[9]=61 // "RESULT_OP="
139 _emit_str(s_rop, 10); _emit_num(check_op)
140 let s_sh: *u8 = sys_mmap(16); s_sh[0]=83; s_sh[1]=72; s_sh[2]=73; s_sh[3]=70; s_sh[4]=84; s_sh[5]=61 // "SHIFT="
141 _emit_str(s_sh, 6); _emit_num(3)
142 let s_it: *u8 = sys_mmap(16); s_it[0]=73; s_it[1]=84; s_it[2]=69; s_it[3]=82; s_it[4]=83; s_it[5]=61 // "ITERS="
143 _emit_str(s_it, 6); _emit_num(iters)
144 let s_to: *u8 = sys_mmap(16); s_to[0]=84; s_to[1]=79; s_to[2]=84; s_to[3]=65; s_to[4]=76; s_to[5]=95; s_to[6]=85; s_to[7]=83; s_to[8]=61 // "TOTAL_US="
145 _emit_str(s_to, 9); _emit_num(total_us)
146 let s_up: *u8 = sys_mmap(24); s_up[0]=85; s_up[1]=83; s_up[2]=95; s_up[3]=80; s_up[4]=69; s_up[5]=82; s_up[6]=95; s_up[7]=79; s_up[8]=80; s_up[9]=95; s_up[10]=78; s_up[11]=83; s_up[12]=61 // "US_PER_OP_NS="
147 _emit_str(s_up, 13); _emit_num(us_per_op_milli)
148 _nl()
149
150 sys_exit(0); return 0
151}