typing/perf 4/5: schema-lifetime lookup structures — interning, memos, env ops, label index - #87
Open
Felipe705x wants to merge 6 commits into
Open
typing/perf 4/5: schema-lifetime lookup structures — interning, memos, env ops, label index#87Felipe705x wants to merge 6 commits into
Felipe705x wants to merge 6 commits into
Conversation
Two cache upgrades that eliminate the remaining clone traffic on hits: - `refine_rc`: the refine memo now stores `Rc<VariableType>`; a hit is a refcount bump instead of a deep clone of the refined descriptor tree. The env operators (which store `Rc` bindings anyway) and the pattern refinement sites consume the Rc directly; `refine()` stays as the by-value API for reference models. `refine_to_nodes` borrow-walks the shared tree and clones only matching Node leaves. - `Schema::junction_nodes`: PathSummary's junction refinement (refine_to_nodes(meet(last, first)) flattened to descriptors) is now memoized on the Schema — cross-hop AND cross-query (a chain reuses one junction at every position). Nested map so lookups clone no keys; kill switch GQLITE_DISABLE_TC_JUNCTION_CACHE; differential test extended to the 2×2 switch matrix. Considered and REJECTED: meet(a,a)=a fast paths — lattice_proptest pins meet idempotence only up to mutual subtyping (explicitly not canonical equality), so short-circuiting could change result structure and warning text. Idle-machine numbers vs the PathSummary state (previous best), all prior states beaten on every case: - chain_1 14.9 → 8.8 us (−41%) chain_16 234 → 141 us (−40%) - chain_dir_16 280 → 147 us (−47%) anydir_8 130 → 94 us (−28%) - union_8 146 → 94 us (−36%) subq_exists 61 → 41.5 us (−32%) - multi_optional 108 → 77 us (−29%) repeat_1_3 15.5 → 10 us (−35%) - anon_16 cliff guard 24.1 → 12.7 ms - check/parse ratios: typical shapes now 4–12× (from 10–30×) vs the original session baseline: chain_1 −55%, chain_16 −69%, subq_exists −40%. Full sweep 80 targets green; hom proptest and 2×2 cache differential green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…che on ids Schema gains a descriptor interner (hash-consing DescriptorType → dense u32, ids stable for the Schema's lifetime). PathSummary boundaries and the junction memo now operate on ids: - dedup/equality in summaries is integer arithmetic (was: eq walks over label trees + property BTreeMaps per insertion), - the junction cache keys by (u32, u32) — a lookup no longer hashes two rich descriptors, - summary clones copy Vec<u32> instead of descriptor trees, - each distinct descriptor pays exactly one hash, at intern time. Ids are schema-scoped; summaries only ever compare within one schema (the only comparison the checker performs). summarize() now takes the schema; hom proptest updated and green (meet/union commute, judgments agree with the live reference). Idle-machine medians vs M05 (all suites + full 80-target sweep green): - chain_1 8.8 → 3.7 us (−58%) chain_16 141 → 64 us (−54%) - chain_dir_16 147 → 73 us (−50%) anydir_8 94 → 54 us (−42%) - union_8 94 → 59 us (−37%) subq_exists 41.5 → 22.6 us (−46%) - multi_optional 77 → 44 us (−43%) repeat_1_3 10 → 3.7 us (−63%) - anon_16 cliff guard 12.7 → 4.2 ms - check/parse ratios on typical shapes: 2.3–8.9× (north star ≤1×) vs session baseline: chain_16 7.0×, subq_exists 3.1×, chain_1 5.2×. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fast path New lattice-op counters (vt_meets, vt_joins, env_bindings_copied) localized the remaining cost precisely: - chains do ZERO VariableType meets — their cost was env cloning: every Concat cloned the accumulated environment, O(vars) String-key clones per hop, quadratic along a chain (272 copies for chain_16); - subq/optional shapes spend their time in real meet/join walks over rich refined types (22–44 per check) — targeted next, not here. Changes: - TypeEnvironment::meet_owned consumes the left env (Concat/Join/ match-chain/subquery folds own their accumulator). Merged bindings stage in a scratch Vec applied on success; the Err path hands the environment back untouched, preserving the exact keep-previous-env-on-error behavior. meet(&,&) remains as the cloning wrapper. - warn_for_collapsed_bindings walked all merged keys; only SHARED keys can collapse (one-sided keys pass through meet untouched, so their "empty" was pre-existing and the old walk skipped them — its one-sided message arms were unreachable). Now takes the stashed shared bindings, warnings byte-identical. - TypeEnvironment::union: Rc::ptr_eq fast path when both arms hold the same shared binding (join(v,v) collapses to v) — common since both arms get the same refine-cache Rc. Idle-machine medians vs M06: - chain_16 64.2 → 46.4 us (−28%) chain_8 28.3 → 22.1 us (−22%) - chain_dir_16 73.4 → 55.1 us (−25%) anydir_8 54.1 → 40.5 us (−25%) - union_8 59.0 → 50.0 us (−15%) - subq_exists / multi_optional unchanged (their cost is the lattice meets; that is the next milestone) - chk/parse: chains now 2.7–4.1× Full sweep 80 targets green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The remaining subquery/OPTIONAL cost was genuine lattice work: 22–44
VariableType::meet/join walks over rich refined types per check. Those
walks are now memoized by interned type id:
- Schema gains a VariableType interner (canonical Rc per distinct
value — first Rc seen wins, which keeps downstream Rc::ptr_eq fast
paths hitting) plus two memos: meet_refine_cache for the env-meet
step refine(meet(a,b)) — including the collapse-error outcome, whose
message regenerates identically from the operand types — and
join_cache for the env-join arms (order-sensitive key, matching
join's structural asymmetry).
- TypeEnvironment bindings become Binding { ty: Rc<VariableType>,
id: Cell<Option<u32>> } — the id is a lazily computed cache, invisible
to the public API (equality still compares types only). meet_owned /
outer_join / union all route through the memos; outer_join's
collapse case keeps the left binding exactly as join(T, Zero) = T did.
- TypeEnvironment::union now takes the schema (checker + module tests
updated).
- Kill switch GQLITE_DISABLE_TC_MEET_CACHE gates both memos;
differential matrix extended (meet-off ≡ all-off row).
Idle-machine medians vs M07:
- subq_exists 22.9 → 15.6 us (−32%) multi_optional 43.6 → 29.0 us (−33%)
- union_8 50.0 → 34.1 us (−32%) union_4 20.8 → 16.6 us (−20%)
- chain_16 46.4 → 42.8 us (−8%) (chains barely meet — expected)
- m07's anon_8 outlier confirmed as noise (back to 1.88 ms)
chk/parse now: trivial cases 0.69–0.95× (AT the north star), chains
2.3–4.2×, subq 4.1×, multi_optional 6.4×.
Cumulative vs session baseline: chain_16 10.5×, subq_exists 4.4×,
union_8 5.3×, multi_optional 4.0×, anon_16 ~5500×.
Full sweep 80 targets green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things, one of which corrects the project's own regression ledger. 1. Schema label index (src/typing/schema_index.rs) — the typechecker's analogue of the runtime's TripleIndex label table: entries bucketed under every positive leaf label; star/neg-free queries scan only buckets(leaves) ∪ fallback. Over-approximation only (the is_subtype filter still decides); candidate order ascending so refined results are bit-identical to the linear scan. Kill switch GQLITE_DISABLE_TC_SCHEMA_INDEX; 512-case adversarial proptest (tests/tc_schema_index_proptest.rs) pins indexed ≡ linear. Measured honestly (interleaved A/B, matched conditions): −1–5% cold on LDBC (one case −14%). Small, because is_subtype already fail-fasts on label mismatch — non-matching entries were never the cold bottleneck. Shipped anyway: zero warm cost, and its value is asymptotic (36 schema entries is tiny; miss cost stays flat as schemas grow). 2. Cold-protocol fix + regression-ledger CORRECTION. `--cold` built a fresh Schema per sample, charging schema-lifetime structures (the index) to every query — a regime that exists nowhere. New Schema::fresh_caches() shares entries/interners/index and clears only the memos: "first sighting of a shape in a live session." Re-measuring with matched interleaved runs (all-cache-switches-off as the old-main proxy, validated against prior measurements) REFUTES the previously reported cold regression: cold-with-caches is 24–69% FASTER than old main, not 7–26% slower — the memos hit within a single query (repeated labels/junctions across positions), and the earlier claim was cross-run thermal noise (±30% on this hardware). There is no regime in which the stack is slower. e_bad_edge_deep (the case the guardrail chart shows the runtime winning): old-main cold ~24.7 us → now 18.4 us cold / 5.3 us warm. Full sweep 81 targets green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Felipe705x
changed the base branch from
tc-perf/07-env-costs
to
tc-perf/04-path-summary
August 29, 2026 21:08
This was referenced Aug 29, 2026
…cope Synthetic N-label schema (own case set; the LDBC-labelled cases would be schema-empty against it). This axis settled the label index's value with order-balanced cold A/Bs: 0±3% at LDBC's 36 entries (the "−1–5% consistent" claim in the previous commit was thermal artifact — this hardware varies ±30% across runs; interleave configs in BOTH orders within one session), −43–57% at N=500. Warm: zero either way (memos answer first). Doc comment now records the measurement discipline. Note: `just lint` on this branch currently fails on an inherited pre-existing clippy-1.98 finding in runtime/ltj/pattern_extract.rs, already fixed on origin/main after this branch forked; resolves on rebase/merge. Typing suites + lib tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #83. Consolidates former #84 (Rc refine cache + junction cache), #85 (descriptor interning), #86 (owning env meet), #87 (type-id meet/join memos), plus the schema label index — five commits, one conceptual move: give the typechecker the same class of build-once lookup privileges the runtime's TripleIndex has always had, at ~10⁻⁵ the memory.
Rc-valued refine cache (hits are refcount bumps) + junction memo, cross-hop and cross-queryPathSummary+ junction cache on denseu32idsCells;refine(meet(a,b))and env joins become integer-pair lookupsschema_index.rs): entries bucketed by leaf label, conservative fallback, bit-identical candidate order, 512-case adversarial proptest. Honest measurement: −1–5% cold (is_subtype already label-fail-fasts); shipped for zero warm cost + flat miss cost as schemas growGQLITE_DISABLE_TC_{REFINE_CACHE,JUNCTION_CACHE,MEET_CACHE,SCHEMA_INDEX}), full differential matrixCumulative vs #83 (idle machine, LDBC SF0.1): chain_16 234 → 43 µs, subq_exists 61 → 15.6 µs, multi_optional 108 → 29 µs, union_8 146 → 34 µs, anon ×16 cliff 24 → 4.2 ms. Trivial queries at/below parse cost.
Regression-ledger correction (final commit): the previously reported "cold first-sighting 7–26% slower" was cross-run thermal noise (±30% on this hardware). Matched interleaved A/B (all switches off = old-main proxy) shows cold-with-caches is 24–69% faster — memos hit within a single query. There is no regime in which this stack is slower.
🤖 Generated with Claude Code