Repository navigation
Conversation
d83f4f1 to
57cbd34
Compare
|
Update: rebased onto Conflict resolution (value.rs only, 2 commits): PR #224 introduced owned vector APIs ( Re-verified after rebase: AI-Tool: Muse Code powered by Meta Muse Spark |
1a092bc to
9c60bfc
Compare
Coverage report: 100.00% (lines)Threshold 100%: PASS (enforced by the quality gates).
Lowest 15 of 99 files. Full HTML report: download the Updated by the quality gates run on every push. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Port the integer-loop accelerator onto the DirectNative master (#226): counted `loop`s over `if`/comparison/`recur` match only validated `Expr::DirectNative` sites (arith/compare ride the `FastArith` tag; `assoc`/`nth`/`=` ride var identity plus the analyzed root's display), and the plan guard replays `direct_valid` per inlined call (no thread binding, fresh epoch, `Arc::ptr_eq` root, with the slow road's cache fill) plus a per-round epoch re-check. Every inlined call observes redefinition exactly like the evaluator's own direct calls. Integer steps compile to postfix ops evaluated with shape-specialized fast paths (leaves and leaf-built steps compute directly; nested runs the iterative machine — no recursion, deeply nested source can't overflow the native stack). Map/vector steps thread one assoc chain through the in-place APIs. Exit branches evaluate via the interpreter against one reused env (rebound in place when unobserved), and a bail after an exit ran resumes the slow loop from the current state instead of rerunning from entry (rerunning would repeat its effects — caught by future_stress_swap: a concurrent `def` bumped the epoch mid-loop). Peeled `map`/`filter` over ranges apply plain closures through a reused frame (`apply_reused`: one arity resolution, in-place rebinds, same depth guard and `recur`-to-fn looping as `apply_closure`). Local-head closure calls skip generic dispatch (`local_closure`). h2h vs JVM Clojure master: 23/23 wins across wall, CPU, and peak RSS (small 12/12, big 11/11). Tests: int_loop/map_loop/vec_loop suites (observance, overflow, capture, bail-resume, exotic reseed), seq_peel reused-frame pins (capture, recur-to-fn, depth parity, ineligible fns), eval unit pins.
e3481e0 to
8664c00
Compare
Port the integer-loop accelerator onto the DirectNative master (#226): counted `loop`s over `if`/comparison/`recur` match only validated `Expr::DirectNative` sites (arith/compare ride the `FastArith` tag; `assoc`/`nth`/`=` ride var identity plus the analyzed root's display), and the plan guard replays `direct_valid` per inlined call (no thread binding, fresh epoch, `Arc::ptr_eq` root, with the slow road's cache fill) plus a per-round epoch re-check. Every inlined call observes redefinition exactly like the evaluator's own direct calls. Integer steps compile to postfix ops evaluated with shape-specialized fast paths (leaves and leaf-built steps compute directly; nested runs the iterative machine — no recursion, deeply nested source can't overflow the native stack). Map/vector steps thread one assoc chain through the in-place APIs. Exit branches evaluate via the interpreter against one reused env (rebound in place when unobserved), and a bail after an exit ran resumes the slow loop from the current state instead of rerunning from entry (rerunning would repeat its effects — caught by future_stress_swap: a concurrent `def` bumped the epoch mid-loop). Peeled `map`/`filter` over ranges apply plain closures through a reused frame (`apply_reused`: one arity resolution, in-place rebinds, same depth guard and `recur`-to-fn looping as `apply_closure`). Local-head closure calls skip generic dispatch (`local_closure`). h2h vs JVM Clojure master: 23/23 wins across wall, CPU, and peak RSS (small 12/12, big 11/11). Tests: int_loop/map_loop/vec_loop suites (observance, overflow, capture, bail-resume, exotic reseed), seq_peel reused-frame pins (capture, recur-to-fn, depth parity, ineligible fns), eval unit pins.
4cff405 to
2bc5372
Compare
Port the integer-loop accelerator onto the DirectNative master (#226): counted `loop`s over `if`/comparison/`recur` match only validated `Expr::DirectNative` sites (arith/compare ride the `FastArith` tag; `assoc`/`nth`/`=` ride var identity plus the analyzed root's display), and the plan guard replays `direct_valid` per inlined call (no thread binding, fresh epoch, `Arc::ptr_eq` root, with the slow road's cache fill) plus a per-round epoch re-check. Every inlined call observes redefinition exactly like the evaluator's own direct calls. Integer steps compile to postfix ops evaluated with shape-specialized fast paths (leaves and leaf-built steps compute directly; nested runs the iterative machine — no recursion, deeply nested source can't overflow the native stack). Map/vector steps thread one assoc chain through the in-place APIs. Exit branches evaluate via the interpreter against one reused env (rebound in place when unobserved), and a bail after an exit ran resumes the slow loop from the current state instead of rerunning from entry (rerunning would repeat its effects — caught by future_stress_swap: a concurrent `def` bumped the epoch mid-loop). Peeled `map`/`filter` over ranges apply plain closures through a reused frame (`apply_reused`: one arity resolution, in-place rebinds, same depth guard and `recur`-to-fn looping as `apply_closure`). Local-head closure calls skip generic dispatch (`local_closure`). h2h vs JVM Clojure master: 23/23 wins across wall, CPU, and peak RSS (small 12/12, big 11/11). Tests: int_loop/map_loop/vec_loop suites (observance, overflow, capture, bail-resume, exotic reseed), seq_peel reused-frame pins (capture, recur-to-fn, depth parity, ineligible fns), eval unit pins.
9cffae5 to
0e8627b
Compare
michael-h-hu
left a comment
There was a problem hiding this comment.
Round 1 review of PR #225 (as michael-h-hu)
Inspected: full diff vs master (origin/master..HEAD, 20 files, ~5.7k lines), all 9 commit messages, PR body, and gh pr checks (all green: quality gates incl. 100% line + patch coverage, 8/8 shards, msrv, loom, fmt+clippy). Locally verified: all 9 commits carry Good id_commit_sign signatures; every st.root = write site in clj-eval goes through an epoch-bumping wrapper (loop-guard soundness vs cross-thread redefinition); re-ran the h2h on the tip — 23/23 cases win on wall+CPU+RSS.
No correctness defects found in the audited hot paths (try_rebind merge, set_nth_owned tuple match, plus_range empty-sum fold, plan guard + epoch re-check, peel recur_pool recycle, run_loop_accel extraction). Two findings below.
High: the branch replaces #234's packing instead of building on it
value.rs deletes MAX_PACKED_LEN (1024, single-run cap from #234, just landed to fix quadratic conj/assoc chains) and substitutes chunked-64 packing, and 1ac12e6c re-pins #234's cap tests to chunked equivalents. Meanwhile #238's measured gains were taken on #234 packing, so this new combination (chunked packing × pooled recur × interp sweep) has no proxy comparison against the master it replaces.
The in-commit rationale (O(1) updates at any size vs cap+generic fallback) is coherent and the h2h wins are verified, so this is not a correctness block — but it needs an explicit decision before landing, one of:
- Adopt #234's packing on this branch and re-verify the h2h table, or
- Keep chunked packing and file a follow-up issue to reconcile/unify the two packing designs with a proxy comparison (quadratic-chain workloads from #234 plus the #238 proxies), linked from the PR.
Low: PR body is stale in two places
- It says "16 commits"; the branch now has 9 after the rebases.
- Point 5 under Solutions ("Arithmetic inlining …
:inlineonly … JVM ignores redefinition for these") contradicts the landed design — the accelerator observes redefinition for every inlined call (unlike JVM:inline), as pinned inint_loop.rs. Please update the body.
Residual risks (non-blocking, noted for the record)
- Narrowest h2h margins are map-build-big (0.82x) and str-build-big (0.83x) RSS — still clear wins, but the ones to watch on Kroki variance.
- This review was produced in the same session as the implementation (self-review): it covers mechanical verification above, not an independent second pair of eyes on the 5.7k-line design. Worth a human skim of
try_int_loop's matcher/guard interplay before landing anything this perf-critical.
AI-Tool: Muse Code (Meta Muse Spark)
Addressed round-1 findings (m-hu)
Gates for the touched area: AI-Tool: Muse Code (Meta Muse Spark) |
michael-h-hu
left a comment
There was a problem hiding this comment.
Round 2 review of PR #225 (as michael-h-hu) — approve
Re-verified round-1 findings against the new tip (7459bd37):
- High (#234 packing divergence) → resolved: author kept chunked-64 with the recorded rationale (O(1) updates at any size, h2h-verified), filed the reconcile/unify follow-up as #240 (open, correctly scoped with proxy comparisons), and pointed at it from the
PACKED_CHUNKdocs. Acceptable: the design decision is explicit, tested, and tracked. - Low (stale PR body) → resolved: commit count corrected, point 5 now describes the landed observant-inlining design, matching
int_loop.rs.
Also confirmed: all PR checks green on the new tip (quality gates incl. 100% line + patch coverage, shards, msrv, loom, fmt+clippy; only tla model check skipping as usual), the new commit is signed, and the shard 2/8 failure on the intermediate run was a runner disk-full flake unrelated to the change (rerun green).
Residual risks (unchanged from round 1): narrowest h2h margins on map-build-big/str-build-big RSS (~0.82x); this loop is a self-review — a human skim of the loop-accelerator matcher/guard interplay is still worthwhile before landing perf-critical code of this size.
AI-Tool: Muse Code (Meta Muse Spark)
Rebase onto 24c8c74 (#226): carry the non-call-dispatch areas forward (packed chunked vectors, range stepping/walking, fused plus-range reduce, map/filter peel chains, swap! fusion, direct Int compare, Inline recur rebind, macro-expansion helper). Call dispatch stays on master's Expr::DirectNative; the loop accelerator rebase follows separately.
Port the integer-loop accelerator onto the DirectNative master (#226): counted `loop`s over `if`/comparison/`recur` match only validated `Expr::DirectNative` sites (arith/compare ride the `FastArith` tag; `assoc`/`nth`/`=` ride var identity plus the analyzed root's display), and the plan guard replays `direct_valid` per inlined call (no thread binding, fresh epoch, `Arc::ptr_eq` root, with the slow road's cache fill) plus a per-round epoch re-check. Every inlined call observes redefinition exactly like the evaluator's own direct calls. Integer steps compile to postfix ops evaluated with shape-specialized fast paths (leaves and leaf-built steps compute directly; nested runs the iterative machine — no recursion, deeply nested source can't overflow the native stack). Map/vector steps thread one assoc chain through the in-place APIs. Exit branches evaluate via the interpreter against one reused env (rebound in place when unobserved), and a bail after an exit ran resumes the slow loop from the current state instead of rerunning from entry (rerunning would repeat its effects — caught by future_stress_swap: a concurrent `def` bumped the epoch mid-loop). Peeled `map`/`filter` over ranges apply plain closures through a reused frame (`apply_reused`: one arity resolution, in-place rebinds, same depth guard and `recur`-to-fn looping as `apply_closure`). Local-head closure calls skip generic dispatch (`local_closure`). h2h vs JVM Clojure master: 23/23 wins across wall, CPU, and peak RSS (small 12/12, big 11/11). Tests: int_loop/map_loop/vec_loop suites (observance, overflow, capture, bail-resume, exotic reseed), seq_peel reused-frame pins (capture, recur-to-fn, depth parity, ineligible fns), eval unit pins.
#234 capped single-run packed vectors at 1024 ints (past the cap they ride the generic trie) to fix quadratic conj/assoc chains on shared runs. This branch packs Int vectors in 64-item chunks instead, so an update touches one chunk however long the run is - the cap has nothing left to do. Re-pin the four #234 tests to the chunked behavior (long runs stay packed, every version intact).
The quality gate flagged two new violations: eval_int_general (cyclomatic 17) and try_fold_leaves (cyclomatic 22). Split the n-ary leaf folds into per-op helpers and the general-machine match into per-op application plus tiny push/step helpers. No behavior change (same ops, same bail points); nested loop tests cover every new line.
The reused-frame peel path (`apply_reused`, `peeled_closure`, `pull_peeled_chunk`) only runs under uniquely-owned walks (`dorun`/`reduce`/friends — `doall` clones, so it always walks generically); the existing pins all used `doall`, leaving the reused arms' entry/recur/throw/depth lines uncovered. Mirror them through `dorun`/`reduce`: ineligible (multi-arity, conveyed, rest-arg) fns, a retained cross-apply capture, fn `recur` (fresh and in-place rebinds), a throw, the reused apply's own depth guard (depth grown purely through peeled self-application), the peeled inexact-tail raise, and the empty 2-arg `+` reduce bail. Loop-guard pins: a thread binding on a loop-called var (via a dynamic alias sharing the `+` native) and a closure redefinition both decline the plan and compute through the slow path. Leaf pins: `inc`/`dec` over a zero-operand leaf, a nested-`inc` step, and a nested-operand `*` fold-mismatch, all computed by the general machine. Two provably-dead lines are restructured instead of pinned: `try_rebind`'s `Patchable` decline becomes an `if let` with fallthrough to the shared fresh-frame `Err` (`recur` binds fresh since #171, so no caller rebinds a `Patchable` frame), and `set_nth_owned` matches (storage, value) once (shape by arm position) instead of guarding plus re-matching.
`with-redefs` to a closure declines the loop plan through the pointer-inequality arm (closures are `Extension`-typed), so the non-`Extension` arm needs a redefinition to plain data: the slow path calls the data and raises exactly like the interpreter. A capture held only by the unwound body does not keep the reused frame observed past the `recur` signal (the `let` frame drops during unwinding), so the observed-`recur` fresh bind needs a capture retained outside (an atom), with every round still running exactly once. No `RangeValue` constructor yields an empty range (every site nil-puns empties to `()`), so the empty 2-arg `plus_range` bail is unreachable: spell the empty sum directly (`(+)` is `0`, identical to the generic walk's `(f)` call) instead of branching on it.
Master's pooled recur changed try_rebind to borrow-and-drain (returning bool): the loop-accelerator exits and the reused-frame peel apply follow suit. The peel recur landing site recycles its drained buffer like the slow loop; the entry and exit rebinds take fresh (never pooled) buffers, so they skip the recycle.
The try_int_loop hook's three-way match pushed eval_loop to cyclomatic 16. Run the accelerator through a helper (Some finished, None runs the slow loop) so eval_loop keeps one branch.
No Clojure-level recur targets a letfn setup frame, so the Shared-check skip in try_rebind is unreachable through eval tests; pin the decline (false, values untouched) directly.
7459bd3 to
39f8708
Compare
Summary
clojure-rs now beats JVM Clojure across the board: 23/23 benchmark cases win on wall time, CPU time, and peak RSS (head of
hhh-pa/clojuremaster incl. PR222 + this branch, vs head ofclojure/clojuremaster, release builds, medians of 2 JVM / 3 RS reps).Branch:
perf/h2h-cpu-mem(9 commits on current master). No Maniphest task or spec doc exists for this work — the acceptance criterion is the head-to-head result table in the Test Plan.Problems
Profiled head-to-head runs (
/tmp/h2h, since removed — numbers below) showed clojure-rs behind JVM Clojure on CPU in most cases and on memory in several, e.g.vec-conj44s (quadratic packed-conjcopies),reduce-range11.3s (per-element seq dispatch over ranges),arith-loop-big12s (interpreter dispatch per round),map-build-big2.7s + 255MB,bubble-big4.0s vs 1.4s (rpds path-copy perassoc),map-filter-big1.59s vs 0.84s (lazy-thunk stepping + Arc churn),atom-doseq-big1.29s vs 1.02s.Root causes
Arcper leaf.first/nextdispatch with fresh tails pernext.loop/recurbodies paid full interpreter dispatch,Envframes, andValueclones per round.map/filterchains over ranges allocated/stepped a lazy thunk per element with mutex + memoization traffic.swap!locked twice per iteration, built an argVec, and generically appliedf.reducecloned accumulator + head per element and dispatched+through generic invoke.Solutions (one per cause, each fails closed to the interpreter)
value.rs): frozen 64-item chunks in an rpds spine + tail → O(1) persistentconj/assocat ~8B/elem; shared big vecs unpack once.seq.rs,seqs.rs): in-placenext_head, chunk tails materialized once,walk_rangehead-at-a-time walk with the same inexact-raise point.eval.rstry_int_loop): shape-matchedif+recurloops run taglessi64rounds; overflow, exotic values, and root-epoch changes bail to the exact interpreter behavior.value.rsinsert_threaded/deep_clone): loop-carried maps assoc in place on a detached copy; pristineassoc-root check + per-round epoch guard sinceassocobserves redefinition.expr.rs/eval.rs):+/</incetc. link directly, but unlike the JVM's:inlinearithmetic every inlined call observes redefinition (plan guard + per-round epoch re-check, pinned inint_loop.rs); everything else stays indirect (probed: JVM observes redefinition forconj/pr/apply).apply_closure— depth guard + conveyed bindings preserved); single-arg frames bind inline with in-placerecurrebind.lazy.rs/seqs.rs): uniquely-ownedmap/filter/removechains over ranges walk fused in 32-rounds — same heads, order, per-chunk checks, and side-effect interleaving as stepping; shared/forced cells decline (peeled shells rebuilt fresh).swap!int fusion (concurrency.rs): known arith natives over int state publish under one lock; validators/watches/non-int/overflow take the generic CAS loop.assoc_int_threaded(copy-on-write chunks, fresh entry identity); steps cover multi-pairassoc,nthreads, andifs over int comparisons.assoc/nthroots verified pristine + epoch re-check; OOB/overflow/generic vectors bail exactly.(reduce + init? range)(seqs.rs): taglessi64accumulation.No regressions
Every fast path declines to the slow path on anything it doesn't model (non-int values, overflow →
long overflowlike the JVM, out-of-bounds, 3-argnth, nested assoc chains, mixed-base steps, redefined roots, validators/watches, shared cells). New pins:int_loop.rs,map_loop.rs,vec_loop.rs,seq_peel.rs,swap_fast.rs,local_apply.rs,direct.rs,packed.rs,map_threaded.rs— including peeled-vs-generic parity, chunk-boundary effect order, entry preservation/identity, bail-error identity, and redefinition observance. Full suite gate green (see Test Plan).Test Plan
cargo fmt --all --check— clean.cargo clippy --locked --no-default-features -p clj-eval --all-targets— no errors/warnings./tmp/h2h/gate.sh, chunked per-suitecargo testover allclj-eval/clj-value/workspace suites incl. the 9 new/changed pin files above) —GATE_FAIL=0.-p clojure-rs) + head-to-head vs JVM master, medians,rs_wall/jvm_wall:AI-Tool: Muse Code powered by Meta Muse Spark