Skip to content

Commit ec0c785

Browse files
BE-251: HashQL: Add benchmarks for MIR transformation passes (#8196)
Co-authored-by: hash-worker[bot] <180894564+hash-worker[bot]@users.noreply.github.com>
1 parent c5d7350 commit ec0c785

File tree

12 files changed

+675
-487
lines changed

12 files changed

+675
-487
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/@local/hashql/mir/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ bstr = { workspace = true }
2222
simple-mermaid = { workspace = true }
2323

2424
[dev-dependencies]
25+
criterion = { workspace = true }
26+
criterion-macro = { workspace = true }
2527
hashql-compiletest = { workspace = true }
2628
insta = { workspace = true }
2729

30+
2831
[lints]
2932
workspace = true
3033

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
#![feature(custom_test_frameworks)]
2+
#![test_runner(criterion::runner)]
3+
#![expect(clippy::min_ident_chars, clippy::many_single_char_names)]
4+
5+
use core::{hint::black_box, time::Duration};
6+
use std::time::Instant;
7+
8+
use criterion::Criterion;
9+
use criterion_macro::criterion;
10+
use hashql_core::{
11+
heap::Heap,
12+
r#type::{TypeBuilder, environment::Environment},
13+
};
14+
use hashql_diagnostics::DiagnosticIssues;
15+
use hashql_mir::{
16+
body::Body,
17+
builder::BodyBuilder,
18+
context::MirContext,
19+
intern::Interner,
20+
op,
21+
pass::{
22+
TransformPass,
23+
transform::{CfgSimplify, DeadStoreElimination},
24+
},
25+
};
26+
27+
/// Creates a simple linear CFG body for benchmarking.
28+
///
29+
/// Structure:
30+
/// ```text
31+
/// bb0: x = 1; goto bb1
32+
/// bb1: y = x == 2; goto bb2
33+
/// bb2: z = y == 3; return z
34+
/// ```
35+
fn create_linear_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap>) -> Body<'heap> {
36+
let mut builder = BodyBuilder::new(interner);
37+
let int_ty = TypeBuilder::synthetic(env).integer();
38+
39+
let x = builder.local("x", int_ty);
40+
let y = builder.local("y", int_ty);
41+
let z = builder.local("z", int_ty);
42+
43+
let const_1 = builder.const_int(1);
44+
let const_2 = builder.const_int(2);
45+
let const_3 = builder.const_int(3);
46+
47+
let bb0 = builder.reserve_block([]);
48+
let bb1 = builder.reserve_block([]);
49+
let bb2 = builder.reserve_block([]);
50+
51+
builder
52+
.build_block(bb0)
53+
.assign_place(x, |rv| rv.load(const_1))
54+
.goto(bb1, []);
55+
56+
builder
57+
.build_block(bb1)
58+
.assign_place(y, |rv| rv.binary(x, op![==], const_2))
59+
.goto(bb2, []);
60+
61+
builder
62+
.build_block(bb2)
63+
.assign_place(z, |rv| rv.binary(y, op![==], const_3))
64+
.ret(z);
65+
66+
builder.finish(0, TypeBuilder::synthetic(env).integer())
67+
}
68+
69+
/// Creates a branching CFG body with a diamond pattern for benchmarking.
70+
///
71+
/// Structure:
72+
/// ```text
73+
/// bb0: switch(cond) -> [0: bb1, 1: bb2]
74+
/// bb1: a = 10; goto bb3
75+
/// bb2: b = 20; goto bb3
76+
/// bb3(p): result = p; return result
77+
/// ```
78+
fn create_diamond_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap>) -> Body<'heap> {
79+
let mut builder = BodyBuilder::new(interner);
80+
let int_ty = TypeBuilder::synthetic(env).integer();
81+
82+
let cond = builder.local("cond", int_ty);
83+
let a = builder.local("a", int_ty);
84+
let b = builder.local("b", int_ty);
85+
let p = builder.local("p", int_ty);
86+
let result = builder.local("result", int_ty);
87+
88+
let const_10 = builder.const_int(10);
89+
let const_20 = builder.const_int(20);
90+
91+
let bb0 = builder.reserve_block([]);
92+
let bb1 = builder.reserve_block([]);
93+
let bb2 = builder.reserve_block([]);
94+
let bb3 = builder.reserve_block([p.local]);
95+
96+
builder
97+
.build_block(bb0)
98+
.switch(cond, |switch| switch.case(0, bb1, []).case(1, bb2, []));
99+
100+
builder
101+
.build_block(bb1)
102+
.assign_place(a, |rv| rv.load(const_10))
103+
.goto(bb3, [a.into()]);
104+
105+
builder
106+
.build_block(bb2)
107+
.assign_place(b, |rv| rv.load(const_20))
108+
.goto(bb3, [b.into()]);
109+
110+
builder
111+
.build_block(bb3)
112+
.assign_place(result, |rv| rv.load(p))
113+
.ret(result);
114+
115+
builder.finish(1, TypeBuilder::synthetic(env).integer())
116+
}
117+
118+
/// Creates a body with dead code for dead store elimination benchmarking.
119+
///
120+
/// Structure:
121+
/// ```text
122+
/// bb0: x = 1; dead1 = 100; dead2 = 200; y = x == 2; return y
123+
/// ```
124+
fn create_dead_store_cfg<'heap>(
125+
env: &Environment<'heap>,
126+
interner: &Interner<'heap>,
127+
) -> Body<'heap> {
128+
let mut builder = BodyBuilder::new(interner);
129+
let int_ty = TypeBuilder::synthetic(env).integer();
130+
131+
let x = builder.local("x", int_ty);
132+
let y = builder.local("y", int_ty);
133+
let dead1 = builder.local("dead1", int_ty);
134+
let dead2 = builder.local("dead2", int_ty);
135+
136+
let const_1 = builder.const_int(1);
137+
let const_2 = builder.const_int(2);
138+
let const_100 = builder.const_int(100);
139+
let const_200 = builder.const_int(200);
140+
141+
let bb0 = builder.reserve_block([]);
142+
143+
builder
144+
.build_block(bb0)
145+
.assign_place(x, |rv| rv.load(const_1))
146+
.assign_place(dead1, |rv| rv.load(const_100))
147+
.assign_place(dead2, |rv| rv.load(const_200))
148+
.assign_place(y, |rv| rv.binary(x, op![==], const_2))
149+
.ret(y);
150+
151+
builder.finish(0, TypeBuilder::synthetic(env).integer())
152+
}
153+
154+
/// Creates a larger CFG with multiple branches and join points for more realistic benchmarking.
155+
///
156+
/// Structure:
157+
/// ```text
158+
/// bb0: switch(cond) -> [0: bb1, 1: bb2, 2: bb3, otherwise: bb4]
159+
/// bb1: a = 1; goto bb5
160+
/// bb2: b = 2; goto bb5
161+
/// bb3: c = 3; goto bb6
162+
/// bb4: d = 4; goto bb6
163+
/// bb5(p1): e = p1 == 10; goto bb7
164+
/// bb6(p2): f = p2 == 20; goto bb7
165+
/// bb7(p3): result = p3; return result
166+
/// ```
167+
fn create_complex_cfg<'heap>(env: &Environment<'heap>, interner: &Interner<'heap>) -> Body<'heap> {
168+
let mut builder = BodyBuilder::new(interner);
169+
let int_ty = TypeBuilder::synthetic(env).integer();
170+
171+
let cond = builder.local("cond", int_ty);
172+
let a = builder.local("a", int_ty);
173+
let b = builder.local("b", int_ty);
174+
let c = builder.local("c", int_ty);
175+
let d = builder.local("d", int_ty);
176+
let e = builder.local("e", int_ty);
177+
let f = builder.local("f", int_ty);
178+
let p1 = builder.local("p1", int_ty);
179+
let p2 = builder.local("p2", int_ty);
180+
let p3 = builder.local("p3", int_ty);
181+
let result = builder.local("result", int_ty);
182+
183+
let const_1 = builder.const_int(1);
184+
let const_2 = builder.const_int(2);
185+
let const_3 = builder.const_int(3);
186+
let const_4 = builder.const_int(4);
187+
let const_10 = builder.const_int(10);
188+
let const_20 = builder.const_int(20);
189+
190+
let bb0 = builder.reserve_block([]);
191+
let bb1 = builder.reserve_block([]);
192+
let bb2 = builder.reserve_block([]);
193+
let bb3 = builder.reserve_block([]);
194+
let bb4 = builder.reserve_block([]);
195+
let bb5 = builder.reserve_block([p1.local]);
196+
let bb6 = builder.reserve_block([p2.local]);
197+
let bb7 = builder.reserve_block([p3.local]);
198+
199+
builder.build_block(bb0).switch(cond, |switch| {
200+
switch
201+
.case(0, bb1, [])
202+
.case(1, bb2, [])
203+
.case(2, bb3, [])
204+
.otherwise(bb4, [])
205+
});
206+
207+
builder
208+
.build_block(bb1)
209+
.assign_place(a, |rv| rv.load(const_1))
210+
.goto(bb5, [a.into()]);
211+
212+
builder
213+
.build_block(bb2)
214+
.assign_place(b, |rv| rv.load(const_2))
215+
.goto(bb5, [b.into()]);
216+
217+
builder
218+
.build_block(bb3)
219+
.assign_place(c, |rv| rv.load(const_3))
220+
.goto(bb6, [c.into()]);
221+
222+
builder
223+
.build_block(bb4)
224+
.assign_place(d, |rv| rv.load(const_4))
225+
.goto(bb6, [d.into()]);
226+
227+
builder
228+
.build_block(bb5)
229+
.assign_place(e, |rv| rv.binary(p1, op![==], const_10))
230+
.goto(bb7, [e.into()]);
231+
232+
builder
233+
.build_block(bb6)
234+
.assign_place(f, |rv| rv.binary(p2, op![==], const_20))
235+
.goto(bb7, [f.into()]);
236+
237+
builder
238+
.build_block(bb7)
239+
.assign_place(result, |rv| rv.load(p3))
240+
.ret(result);
241+
242+
builder.finish(1, TypeBuilder::synthetic(env).integer())
243+
}
244+
245+
fn run_fn(
246+
iters: u64,
247+
body: for<'heap> fn(&Environment<'heap>, &Interner<'heap>) -> Body<'heap>,
248+
mut func: impl for<'env, 'heap> FnMut(&mut MirContext<'env, 'heap>, &mut Body<'heap>),
249+
) -> Duration {
250+
let mut heap = Heap::new();
251+
let mut total = Duration::ZERO;
252+
253+
for _ in 0..iters {
254+
heap.reset();
255+
let env = Environment::new(&heap);
256+
let interner = Interner::new(&heap);
257+
let mut body = black_box(body(&env, &interner));
258+
259+
let mut context = MirContext {
260+
heap: &heap,
261+
env: &env,
262+
interner: &interner,
263+
diagnostics: DiagnosticIssues::new(),
264+
};
265+
266+
let start = Instant::now();
267+
func(&mut context, &mut body);
268+
total += start.elapsed();
269+
270+
drop(black_box(body));
271+
}
272+
273+
total
274+
}
275+
276+
fn run(
277+
iters: u64,
278+
body: for<'heap> fn(&Environment<'heap>, &Interner<'heap>) -> Body<'heap>,
279+
mut pass: impl for<'env, 'heap> TransformPass<'env, 'heap>,
280+
) -> Duration {
281+
run_fn(
282+
iters,
283+
body,
284+
#[inline]
285+
|context, body| pass.run(context, body),
286+
)
287+
}
288+
289+
#[criterion]
290+
fn cfg_simplify(criterion: &mut Criterion) {
291+
let mut group = criterion.benchmark_group("cfg_simplify");
292+
293+
group.bench_function("linear", |bencher| {
294+
bencher.iter_custom(|iters| run(iters, create_linear_cfg, CfgSimplify::new()));
295+
});
296+
group.bench_function("diamond", |bencher| {
297+
bencher.iter_custom(|iters| run(iters, create_diamond_cfg, CfgSimplify::new()));
298+
});
299+
group.bench_function("complex", |bencher| {
300+
bencher.iter_custom(|iters| run(iters, create_complex_cfg, CfgSimplify::new()));
301+
});
302+
}
303+
304+
#[criterion]
305+
fn dse(criterion: &mut Criterion) {
306+
let mut group = criterion.benchmark_group("dse");
307+
308+
group.bench_function("dead stores", |bencher| {
309+
bencher.iter_custom(|iters| run(iters, create_dead_store_cfg, DeadStoreElimination::new()));
310+
});
311+
group.bench_function("linear", |bencher| {
312+
bencher.iter_custom(|iters| run(iters, create_linear_cfg, DeadStoreElimination::new()));
313+
});
314+
group.bench_function("diamond", |bencher| {
315+
bencher.iter_custom(|iters| run(iters, create_diamond_cfg, DeadStoreElimination::new()));
316+
});
317+
group.bench_function("complex", |bencher| {
318+
bencher.iter_custom(|iters| run(iters, create_complex_cfg, DeadStoreElimination::new()));
319+
});
320+
}
321+
322+
#[criterion]
323+
fn pipeline(criterion: &mut Criterion) {
324+
let mut group = criterion.benchmark_group("pipeline");
325+
326+
group.bench_function("linear", |bencher| {
327+
let mut cfg = CfgSimplify::new();
328+
let mut dse = DeadStoreElimination::new();
329+
330+
bencher.iter_custom(|iters| {
331+
run_fn(iters, create_linear_cfg, |context, body| {
332+
cfg.run(context, body);
333+
dse.run(context, body);
334+
})
335+
});
336+
});
337+
group.bench_function("diamond", |bencher| {
338+
let mut cfg = CfgSimplify::new();
339+
let mut dse = DeadStoreElimination::new();
340+
341+
bencher.iter_custom(|iters| {
342+
run_fn(iters, create_diamond_cfg, |context, body| {
343+
cfg.run(context, body);
344+
dse.run(context, body);
345+
})
346+
});
347+
});
348+
group.bench_function("complex", |bencher| {
349+
let mut cfg = CfgSimplify::new();
350+
let mut dse = DeadStoreElimination::new();
351+
352+
bencher.iter_custom(|iters| {
353+
run_fn(iters, create_complex_cfg, |context, body| {
354+
cfg.run(context, body);
355+
dse.run(context, body);
356+
})
357+
});
358+
});
359+
}

0 commit comments

Comments
 (0)