Skip to content

perf(typecheck): compare = guards over engine terms in the arena (b-typed ~4.3x less CPU) - #96

Merged
pyrex41 merged 1 commit into
mainfrom
perf/eqterm-native-equality
Oct 21, 2026
Merged

pyrex41 merged 1 commit into
mainfrom
perf/eqterm-native-equality

Conversation

@pyrex41

@pyrex41 pyrex41 commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

The native Prolog translator lifts every Shen expression embedded in a rule, such as a when test or an is right-hand side, into a KL defun. It calls that defun with its engine-term arguments materialized, meaning deep-copied out to fresh Shen lists. One of those guards is shen.by-hypothesis's check, (when (= (shen.deref X B) (shen.deref Y B))), and it runs once per hypothesis per goal. On capcov's typecheck-heavy b-typed workload, materialize + R.cons + the GC they cause took ~85% of samples (LuaJIT jit.p), with GC alone at 34%.

lift_guard now compiles (= T1 T2), where each side is (shen.deref X B) or a bare term variable, to E.eqterm. That is a lazyderef walk over the arena that gives the same answer as (= (materialize a) (materialize b)) and allocates nothing.

The bare-variable form matters for a 43.1 kernel with the typecheck deref hoist (shen-sources perf/typecheck-deref-hoist). There, by-hypothesis-h compares the already-dereferenced X as (= X (shen.deref Y B)). This PR alone does not make shen-lua run that kernel: its new shen.by-hypothesis-h driver is missing from DRIVER_FNS. That is fixed in the stacked PR (kernel robustness).

Results

b-typed CPU (user+sys) from /usr/bin/time -l on the binary, 2 interleaved rounds, LuaJIT 2.1.1785763465, payload byte-identical to go-1f587de in every run:

build r1 CPU r2 CPU instructions r1 / r2 load (1-min avg)
origin/main dcfe548 49.71 s 46.34 s 667.8 G / 654.4 G 32→34 / 39→40
this PR 10.40 s 12.17 s 139.1 G / 140.5 G 34→36 / 39→39

That is -79% instructions retired (661 G → 140 G mean, 4.7x) and about 4.3x less CPU (48.0 s → 11.3 s mean).

Gates (this branch, vendored 43.1)

  • make test: TOTAL 1774 pass, 0 fail across 28 specs (origin/main: 1762 / 27). This adds test/eqterm_spec.lua, which checks:
    • eqterm against = on all 841 pairs of a mixed term set;
    • 200000-deep nesting;
    • the translator emitting EQTERM for both guard spellings and keeping the lifted path for other guards;
    • end-to-end typechecks.
  • run-kernel-tests.lua: 154 passed, 0 failed, kerneltests result: ok.
  • capcov workloads: all 8 payloads byte-identical to go-1f587de (a0, a1, a2, b-typed, c-untyped, d-g5, e-draft, f-modelcheck).

🤖 Generated with Claude Code

The native Prolog translator lifted every embedded Shen expression into a
KL defun called with its term arguments materialized, so the
by-hypothesis check (= (shen.deref X B) (shen.deref Y B)) copied the goal
and each hypothesis out to fresh Shen lists once per hypothesis per goal.
On capcov's b-typed workload that copying (plus the GC it causes) was
~85% of samples.

lift_guard now compiles (= T1 T2), where each side is (shen.deref X B) or
a bare term variable, to E.eqterm: a lazyderef walk over the arena that
returns the same answer as (= (materialize a) (materialize b)) without
allocating. Distinct atoms/vars still defer to the kernel's =, so tuples
and vectors keep structural equality; car recursion is counted on the
#93 stack segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pyrex41 pyrex41 changed the title perf(typecheck): compare = guards over engine terms in the arena (b-typed ~4.4x less CPU) perf(typecheck): compare = guards over engine terms in the arena (b-typed ~4.3x less CPU) Oct 21, 2026
@pyrex41
pyrex41 merged commit a4c3158 into main Oct 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant