Files
AILang/bench/mono_dispatch.py
Brummel 9fdc4cacff iter form-a.1 (Tasks 6-12): milestone close
Second half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All seven tasks DONE; cargo
test --workspace green at every per-task boundary.

T6 — Bench-driver suffix flip from .ail.json to .ail across 4
Python scripts + run.sh. compile_check.py + cross_lang.py exit 0.

T7 — Re-author e2e.rs raw-JSON-inspect tests:
- diff_detects_changed_def — derive sum.ail.json on-the-fly via
  `ail parse examples/sum.ail` into tempdir, then mutate + diff.
- borrow_own_demo_modes_are_metadata_only — same pattern.
- reuse_as_demo_under_rc_uses_inplace_rewrite — same pattern.
- render_parse_round_trip_canonical — RETIRED (subsumed by T1's
  cli_parse_then_render_then_parse_is_idempotent over whole corpus).
- ail_run_accepts_ail_source_with_same_stdout_as_ail_json —
  re-authored to derive hello.ail.json in a per-process tempdir
  from hello.ail via `ail parse`, then assert dual-form stdout.

T8 — Bulk-delete 156 non-carve-out .ail.json. Inventory:
8 .ail.json (carve-outs, alphabetical: broken_unbound + prelude
+ 3× test_22b2_* + 3× test_ct1_*) + 157 .ail. carve_out_inventory
test un-#[ignore]'d and green. Forward-pulled 20 repairs that the
T1-5 dispatch's recon missed (12 Group-B suffix + 5 Group-A
load_workspace + 3 ail_run sites). Also forward-pulled T9 Step 5
(schema_coverage corpus flip from .ail.json to .ail) to satisfy
T8's green-gate.

T9 — Retire obsolete roundtrip tests:
- print_then_parse_round_trips_every_fixture (round_trip.rs)
- every_ail_fixture_matches_its_json_counterpart (round_trip.rs)
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
- Dead helpers: list_json_fixtures ×2, round_trip_one,
  strip_trailing_newlines.
Schema-coverage corpus already flipped in T8 (forward-pull).

T10 — DESIGN.md §"Roundtrip Invariant" (lines 2027-2109) restated
with parse-determinism + idempotency + CLI-pipeline-idempotency +
carve-out-anchor framing. Five surviving enforcement tests named.
§"Float literals" and §"Why anchored at top level" preserved.

T11 — §A4 doctrine edits: CLAUDE.md:5-6 + DESIGN.md:465-466.
Canonical form remains JSON-AST; authoring projection is .ail;
build derives JSON-AST in-process via ailang_surface::parse.

T12 — Milestone close:
- WhatsNew entry: user-facing language, lead with the change.
- Roadmap: [milestone] form-a struck [x] with closing note.
- Final inventory verified: 8 .ail.json + 157 .ail.
- Final cargo test --workspace: 557 passed, 0 failed, 3 ignored.
- bench/compile_check.py + bench/cross_lang.py: exit 0.

Test math: pre-iter 558 baseline + 3 new T1 tests = 561, − 1
(T7 retire) − 3 (T9 retire) = 557 final.

INDEX.md appended with the full iter summary covering T1-T12 (the
T1-5 commit at 77b28ad deferred the INDEX line to full-iter close).

Milestone [Form-A as the default authoring surface] structurally
closed. The compile-time-embed carve-out (prelude.ail.json) is
the subject of the queued follow-up milestone [Prelude embed:
Form-A as compile-time source]. audit-form-a runs as the next
dispatch.
2026-05-13 11:31:39 +02:00

195 lines
8.4 KiB
Python
Executable File

#!/usr/bin/env python3
# Mono-vs-virtual-dispatch micro-bench (one-off, hypothesis-driven).
#
# Hypothesis (H1): Monomorphised class-method calls in AILang are
# measurably faster than the equivalent function-pointer-indirected
# variant on a tight hot loop.
#
# What this harness measures:
# - AILang mono'd binary at -O2 --alloc=rc on
# examples/bench_mono_dispatch.ail
# - Hand-C reference, three variants:
# direct-inlinable — foo() inlinable, direct call
# direct-noinline — foo() noinline, direct call
# indirect — foo() noinline, called via volatile fnptr
#
# All four binaries run the same algorithm: a 100M-iter tail-recursive
# loop accumulating `acc + foo(acc + i)` where `foo(x) = x*1103515245
# + 12345`. The loop has a serial data dependency on `acc` so clang
# cannot algebraically close-form-fold it.
#
# Ratios reported (N runs, slowest dropped, median of the rest):
#
# ail / direct-inlinable — codegen-quality gap. Should be ~1.0x
# if mono produces an equivalent IR.
# direct-noinline / direct-inlinable
# — inlining benefit when callee is
# statically known.
# indirect / direct-noinline
# — THE actual mono-vs-vdisp delta on this
# hardware when both arms deny inlining.
# ail / indirect — end-to-end win: real workload (mono'd
# AILang, where LLVM CAN inline) vs
# hypothetical fnptr-dispatched AILang.
#
# What this bench does NOT show:
# - It does not measure dictionary-passing in any literal sense —
# AILang has no dict-passing implementation. The fnptr indirection
# stands in for "dispatch through an opaque target", which is the
# load-bearing optimiser barrier any vdisp scheme imposes.
# - It does not measure RC traffic. The fixture uses Int args (no
# heap allocation), so RC is a no-op. This is intentional: we
# are measuring DISPATCH cost, not RC cost. Dictionary RC traffic
# under a hypothetical vdisp implementation would be additional
# overhead on top of what `indirect` measures here.
# - It does not exercise polymorphic call sites with multiple
# instances (megamorphic dispatch). The fnptr is monomorphic
# in the C indirect variant; a multi-instance bench would
# produce a larger indirect/direct gap due to predictor misses.
#
# Usage: bench/mono_dispatch.py [-n RUNS]
# -n RUNS number of timed runs per binary (default 7; min 3).
from __future__ import annotations
import argparse
import statistics
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
AIL = ROOT / "target" / "release" / "ail"
EXAMPLES = ROOT / "examples"
REFERENCE = ROOT / "bench" / "reference"
OUTDIR = ROOT / "target" / "bench_mono"
EXPECTED = "-2551317978420243992"
def time_one(bin_path: Path) -> tuple[float, str]:
"""Run binary, return (wall_seconds, stdout). Stdout captured for
correctness check (all four binaries must print the same value)."""
t0 = time.monotonic()
proc = subprocess.run([str(bin_path)], capture_output=True, text=True)
t1 = time.monotonic()
if proc.returncode != 0:
raise RuntimeError(f"{bin_path} exited {proc.returncode}: {proc.stderr}")
return (t1 - t0, proc.stdout.strip())
def median_drop_slowest(runs: list[float]) -> dict[str, float]:
if len(runs) < 2:
return {"min": runs[0], "median": runs[0], "max": runs[0]}
kept = sorted(runs)[:-1]
return {
"min": min(kept),
"median": statistics.median(kept),
"max": max(kept),
}
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("-n", "--runs", type=int, default=7,
help="runs per binary; min 3, default 7")
args = ap.parse_args()
if args.runs < 3:
print("--runs must be >= 3", file=sys.stderr)
return 2
OUTDIR.mkdir(parents=True, exist_ok=True)
# Build AILang fixture if needed.
if not AIL.is_file():
subprocess.run(["cargo", "build", "--release", "-p", "ail"],
cwd=str(ROOT), check=True)
print(">>> building AILang fixture (-O2, --alloc=rc)", file=sys.stderr)
ail_bin = OUTDIR / "bench_mono_dispatch_ail"
subprocess.run(
[str(AIL), "build", "--opt=-O2", "--alloc=rc",
str(EXAMPLES / "bench_mono_dispatch.ail"), "-o", str(ail_bin)],
check=True, capture_output=True,
)
# Build the three C references.
print(">>> building C references (clang -O2)", file=sys.stderr)
# `indirect-polymorphic` produces a DIFFERENT stdout (4 distinct
# foo bodies → different acc); the harness skips the equality
# check for it but still times it.
c_specs = [
("direct-inlinable", "bench_mono_direct.c", True),
("direct-noinline", "bench_mono_direct_noinline.c", True),
("indirect", "bench_mono_indirect.c", True),
("indirect-polymorphic", "bench_mono_indirect_polymorphic.c", False),
]
c_bins: dict[str, tuple[Path, bool]] = {}
for label, src, equality_check in c_specs:
bin_path = OUTDIR / f"bench_mono_{label.replace('-', '_')}"
subprocess.run(
["clang", "-O2", "-o", str(bin_path), str(REFERENCE / src)],
check=True, capture_output=True,
)
c_bins[label] = (bin_path, equality_check)
# Equality check: ail-mono + the three monomorphic-foo C variants
# must all print the EXPECTED value. The polymorphic variant has
# different `foo` semantics → different acc; we skip the equality
# check for it.
print(">>> correctness check", file=sys.stderr)
binaries: list[tuple[str, Path, bool]] = [("ail-mono", ail_bin, True)]
binaries.extend((label, path, eq) for label, (path, eq) in c_bins.items())
for label, path, eq in binaries:
_, out = time_one(path)
if eq and out != EXPECTED:
print(f"correctness fail: {label} produced {out!r}, expected {EXPECTED!r}",
file=sys.stderr)
return 1
if not eq:
print(f" {label} stdout = {out} (equality check skipped)", file=sys.stderr)
print(f" monomorphic-foo binaries produce {EXPECTED}", file=sys.stderr)
# Time each binary RUNS times.
print(f">>> timing (runs={args.runs}, slowest dropped)", file=sys.stderr)
timings: dict[str, dict[str, float]] = {}
for label, path, _eq in binaries:
runs = [time_one(path)[0] for _ in range(args.runs)]
timings[label] = median_drop_slowest(runs)
timings[label]["raw"] = runs # full raw data for the report
# Report.
print()
print(f"=== bench_mono_dispatch ({args.runs} runs each, slowest dropped, all times in seconds) ===")
print()
print(f"{'binary':<24} {'min':>8} {'median':>8} {'max':>8} raw runs")
print("-" * 110)
for label, _, _eq in binaries:
t = timings[label]
raw_str = " ".join(f"{r:.3f}" for r in t["raw"])
print(f"{label:<24} {t['min']:8.4f} {t['median']:8.4f} {t['max']:8.4f} [{raw_str}]")
# Ratios (always median-over-median).
print()
print("=== ratios (median over median) ===")
print()
ail_med = timings["ail-mono"]["median"]
di_med = timings["direct-inlinable"]["median"]
dn_med = timings["direct-noinline"]["median"]
ind_med = timings["indirect"]["median"]
poly_med = timings["indirect-polymorphic"]["median"]
print(f" ail-mono / direct-inlinable = {ail_med / di_med:6.3f}x (codegen-quality gap)")
print(f" direct-noinline / direct-inlinable = {dn_med / di_med:6.3f}x (inlining benefit)")
print(f" indirect / direct-noinline = {ind_med / dn_med:6.3f}x (monomorphic vdisp delta)")
print(f" indirect-polymorphic / direct-noinline = {poly_med / dn_med:6.3f}x (polymorphic vdisp delta)")
print(f" indirect-polymorphic / indirect = {poly_med / ind_med:6.3f}x (predictor-miss penalty)")
print(f" ail-mono / indirect = {ail_med / ind_med:6.3f}x (end-to-end vs mono indirect)")
print(f" ail-mono / indirect-polymorphic = {ail_med / poly_med:6.3f}x (end-to-end vs poly indirect)")
print()
return 0
if __name__ == "__main__":
sys.exit(main())