Skip to content

Commit f7d5f3a

Browse files
authored
Merge pull request #52 from rail44/claude/plan-next-tasks-GjrNi
bench: 旧 API ベースの interp ベンチを直して TCO / JIT 比較を追加
2 parents acf018b + e0704cf commit f7d5f3a

4 files changed

Lines changed: 140 additions & 40 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ AST → Cranelift IR 直行で native コード生成。tree-walker は referenc
9191

9292
- ✅ 型変数を `α/β/γ` に rename して表示 — done 2026-05-17(PR #45
9393
- ✅ 64MB stack hack を **8MB に縮小 + TCO 実装** — done 2026-05-17(PR #51)。`interpret` を loop ベースに書き直し、`Call` / `If` / `ImmediateBlock` の tail-position 遷移は `cur` ポインタ更新 + `continue` で Rust スタックを消費しない。tail-recursive `loop_n(1_000_000, 0)` が 8MB スタックで通る。非 tail 再帰(`count(n) => ... count(n-1) + 1`)は依然として 1 spctr フレーム ≈ 1.5KB の Rust スタックを食うので、完全撤廃には full iterative trampoline が必要(将来課題)。
94-
- ベンチ充実(より多角的な性能測定)
94+
- ベンチ充実 — done 2026-05-17。`benches/interp.rs` を旧 Iterator API から List/String/Number stdlib ベースに書き直し。`bench_tail_recursion`(TCO 効果測定)と `bench_stdlib_reduce`(JIT inline `List.reduce` 計測)を追加。同 fib / tail-rec / reduce ソースを tree-walker / JIT 両方で測定するように対比形式に。pre-compile 用に `jit::compile` 関数を新規公開(ベンチで b.iter 外で 1 回コンパイルしてから繰り返し走らせる、leak を回避)。直近の実測:fib(25) 94x、tail-rec 100k loop 20x、sum_range 10k 4.6x の JIT スピードアップ。
9595
- エラーメッセージの polish
9696

9797
**コスト**:小〜中

benches/interp.rs

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,122 @@
1+
//! Benchmarks for tree-walker and Cranelift JIT.
2+
//!
3+
//! We focus on:
4+
//!
5+
//! - **fib** — classic deep non-tail recursion (call-overhead dominated).
6+
//! - **tail_recursion** — accumulator-style loop. Before TCO landed this
7+
//! required the 64MB stack hack; now both backends loop in O(1) stack.
8+
//! - **stdlib_reduce** — exercises the inline-compiled `List.reduce`
9+
//! helper in the JIT.
10+
//! - **fizzbuzz** — `List.map` over a range with string interpolation
11+
//! (tree-walker only — `jit::run` is numeric-result-only).
12+
//! - **parse** — front-end only, to track parser changes in isolation.
13+
//!
14+
//! For JIT timings we compile once outside `b.iter` and only measure the
15+
//! actual `Compiled::run`. Compiling inside `iter` would both inflate the
16+
//! number with codegen cost and leak executable pages every iteration —
17+
//! `jit::run` is fire-and-forget on purpose.
118
use criterion::{criterion_group, criterion_main, Criterion};
2-
use spctr::{interp, parser, resolver};
19+
use spctr::{interp, jit, parser, resolver};
320
use std::hint::black_box;
421

5-
fn run_program(src: &str) {
22+
fn parse_and_resolve(src: &str) -> spctr::ast::Statement {
623
let ast = parser::parse(src).expect("parse failed");
724
resolver::resolve(&ast, &interp::ROOT_NAMES).expect("resolve failed");
8-
let v = interp::run(&ast).expect("interpret failed");
9-
black_box(v);
25+
ast
1026
}
1127

1228
fn bench_fib(c: &mut Criterion) {
1329
let mut group = c.benchmark_group("fib");
1430
for n in [10u32, 20, 25] {
1531
let src = format!(
16-
"fib: (n) => if n < 2 then n else fib(n-1) + fib(n-2), fib({})",
32+
"fib: (n) => if n < 2 then n else fib(n - 1) + fib(n - 2), fib({})",
1733
n
1834
);
19-
group.bench_function(format!("fib({})", n), |b| {
20-
b.iter(|| run_program(&src));
35+
let ast = parse_and_resolve(&src);
36+
let compiled = jit::compile(&ast).expect("jit compile");
37+
group.bench_function(format!("interp/fib({})", n), |b| {
38+
b.iter(|| black_box(interp::run(&ast).expect("interp")));
39+
});
40+
group.bench_function(format!("jit/fib({})", n), |b| {
41+
b.iter(|| black_box(compiled.run()));
42+
});
43+
}
44+
group.finish();
45+
}
46+
47+
fn bench_tail_recursion(c: &mut Criterion) {
48+
let mut group = c.benchmark_group("tail_recursion");
49+
for n in [1_000u32, 10_000, 100_000] {
50+
let src = format!(
51+
"loop_n: (n, acc) => if n == 0 then acc else loop_n(n - 1, acc + 1), loop_n({}, 0)",
52+
n
53+
);
54+
let ast = parse_and_resolve(&src);
55+
let compiled = jit::compile(&ast).expect("jit compile");
56+
group.bench_function(format!("interp/loop({})", n), |b| {
57+
b.iter(|| black_box(interp::run(&ast).expect("interp")));
58+
});
59+
group.bench_function(format!("jit/loop({})", n), |b| {
60+
b.iter(|| black_box(compiled.run()));
61+
});
62+
}
63+
group.finish();
64+
}
65+
66+
fn bench_stdlib_reduce(c: &mut Criterion) {
67+
let mut group = c.benchmark_group("stdlib_reduce");
68+
for n in [100u32, 1_000, 10_000] {
69+
let src = format!(
70+
"List.reduce(List.range(0, {}), 0, (acc, x) => acc + x)",
71+
n
72+
);
73+
let ast = parse_and_resolve(&src);
74+
let compiled = jit::compile(&ast).expect("jit compile");
75+
group.bench_function(format!("interp/sum_range({})", n), |b| {
76+
b.iter(|| black_box(interp::run(&ast).expect("interp")));
77+
});
78+
group.bench_function(format!("jit/sum_range({})", n), |b| {
79+
b.iter(|| black_box(compiled.run()));
2180
});
2281
}
2382
group.finish();
2483
}
2584

2685
fn bench_fizzbuzz(c: &mut Criterion) {
2786
let mut group = c.benchmark_group("fizzbuzz");
28-
for n in [100u32, 1000] {
87+
for n in [100u32, 1_000] {
2988
let src = format!(
3089
r#"
31-
range: Iterator.range(0, {}),
32-
fizzbuzz: (i) => {{
33-
is_fizz: i % 3 == 0,
34-
is_buzz: i % 5 == 0,
35-
fizz: if is_fizz then "fizz" else "",
36-
buzz: if is_buzz then "buzz" else "",
37-
String.concat(fizz, buzz)
38-
}},
39-
range.map((i) => [i, fizzbuzz(i)]).to_list
90+
fizzbuzz: (i) =>
91+
if i % 15 == 0 then "FizzBuzz"
92+
else if i % 3 == 0 then "Fizz"
93+
else if i % 5 == 0 then "Buzz"
94+
else "${{i}}",
95+
List.map(List.range(0, {}), fizzbuzz)
4096
"#,
4197
n
4298
);
43-
group.bench_function(format!("fizzbuzz({})", n), |b| {
44-
b.iter(|| run_program(&src));
99+
let ast = parse_and_resolve(&src);
100+
group.bench_function(format!("interp/fizzbuzz({})", n), |b| {
101+
b.iter(|| black_box(interp::run(&ast).expect("interp")));
45102
});
46103
}
47104
group.finish();
48105
}
49106

50107
fn bench_parse_only(c: &mut Criterion) {
51108
let src = include_str!("../examples/fizzbuzz.spc");
52-
c.bench_function("parse fizzbuzz.spc", |b| {
53-
b.iter(|| {
54-
let ast = parser::parse(black_box(src)).expect("parse");
55-
black_box(ast);
56-
});
109+
c.bench_function("parse/fizzbuzz.spc", |b| {
110+
b.iter(|| black_box(parser::parse(black_box(src)).expect("parse")));
57111
});
58112
}
59113

60-
criterion_group!(benches, bench_fib, bench_fizzbuzz, bench_parse_only);
114+
criterion_group!(
115+
benches,
116+
bench_fib,
117+
bench_tail_recursion,
118+
bench_stdlib_reduce,
119+
bench_fizzbuzz,
120+
bench_parse_only,
121+
);
61122
criterion_main!(benches);

examples/fizzbuzz.spc

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
range: Iterator.range(0, 9000),
1+
// FizzBuzz over [0, 100). Returns a list of strings — number-typed slots
2+
// would force a heterogeneous list, so `i` is interpolated as a string.
3+
fizzbuzz: (i) =>
4+
if i % 15 == 0 then "FizzBuzz"
5+
else if i % 3 == 0 then "Fizz"
6+
else if i % 5 == 0 then "Buzz"
7+
else "${i}",
28

3-
fizzbuzz: (i) => {
4-
is_fizz: i % 3 = 0,
5-
is_buzz: i % 5 = 0,
6-
fizz: if is_fizz "fizz" else "",
7-
buzz: if is_buzz "buzz" else "",
8-
9-
String.concat(fizz, buzz)
10-
},
11-
12-
range.map((i) => [i, fizzbuzz(i)]).to_list
9+
List.map(List.range(0, 100), fizzbuzz)

src/jit.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,36 @@ pub fn run_with_display(ast: &Statement) -> Result<(), Diagnostic> {
276276
run_inner(ast, true).map(|_| ())
277277
}
278278

279-
fn run_inner(ast: &Statement, display: bool) -> Result<f64, Diagnostic> {
279+
/// A handle to a compiled spctr program ready to invoke. The module owning
280+
/// the JITed executable memory is kept alive for as long as the `Compiled`
281+
/// is alive — drop the handle to release the memory.
282+
///
283+
/// Useful for benchmarking (compile once, run many times) and for embedding
284+
/// where the program will be called repeatedly without re-parsing.
285+
pub struct Compiled {
286+
main_fn: extern "C" fn() -> f64,
287+
// SAFETY anchor: `main_fn` is a raw function pointer into the executable
288+
// pages owned by `_module`. The module must outlive every call to
289+
// `main_fn`, so we hold it here.
290+
_module: JITModule,
291+
}
292+
293+
impl Compiled {
294+
/// Invoke the compiled program. Returns the numeric result (or a
295+
/// sentinel `0.0` when the program was compiled in display mode).
296+
pub fn run(&self) -> f64 {
297+
(self.main_fn)()
298+
}
299+
}
300+
301+
/// Compile an AST into a `Compiled` handle without executing it. The
302+
/// returned handle is independent of the AST and can be invoked
303+
/// repeatedly. Most users want [`run`] or [`run_with_display`] instead.
304+
pub fn compile(ast: &Statement) -> Result<Compiled, Diagnostic> {
305+
compile_inner(ast, false)
306+
}
307+
308+
fn compile_inner(ast: &Statement, display: bool) -> Result<Compiled, Diagnostic> {
280309
let tres = typeck::check(ast, &interp::root_types());
281310
if let Some(w) = tres.warnings.into_iter().next() {
282311
return Err(w);
@@ -292,8 +321,21 @@ fn run_inner(ast: &Statement, display: bool) -> Result<f64, Diagnostic> {
292321
.map_err(|e| internal(format!("finalize: {e}")))?;
293322
let main_ptr = module.get_finalized_function(main_id);
294323
let main_fn: extern "C" fn() -> f64 = unsafe { std::mem::transmute(main_ptr) };
295-
let result = main_fn();
296-
std::mem::forget(module);
324+
Ok(Compiled {
325+
main_fn,
326+
_module: module,
327+
})
328+
}
329+
330+
fn run_inner(ast: &Statement, display: bool) -> Result<f64, Diagnostic> {
331+
let compiled = compile_inner(ast, display)?;
332+
let result = compiled.run();
333+
// Forget the module: spctr programs leak top-level closure allocations
334+
// via `Box::leak` and these point into the module's executable pages;
335+
// dropping the module would invalidate them. Callers that want to
336+
// reclaim memory should use `compile` + `Compiled` and drop the handle
337+
// themselves.
338+
std::mem::forget(compiled);
297339
Ok(result)
298340
}
299341

0 commit comments

Comments
 (0)