Skip to content

typing/perf 4/5: schema-lifetime lookup structures — interning, memos, env ops, label index - #87

Open
Felipe705x wants to merge 6 commits into
tc-perf/04-path-summaryfrom
tc-perf/08-meet-memo
Open

typing/perf 4/5: schema-lifetime lookup structures — interning, memos, env ops, label index#87
Felipe705x wants to merge 6 commits into
tc-perf/04-path-summaryfrom
tc-perf/08-meet-memo

Conversation

@Felipe705x

@Felipe705x Felipe705x commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

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-query
  • Descriptor interning: PathSummary + junction cache on dense u32 ids
  • Owning env meet: quadratic env cloning eliminated (272 String-key copies for chain_16 → 0); collapse warnings byte-identical via shared-keys-only walk
  • Type interning + meet/join memos: bindings carry lazy id Cells; refine(meet(a,b)) and env joins become integer-pair lookups
  • Schema label index (schema_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 grow
  • All kill-switched (GQLITE_DISABLE_TC_{REFINE_CACHE,JUNCTION_CACHE,MEET_CACHE,SCHEMA_INDEX}), full differential matrix

Cumulative 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

Felipe705x and others added 4 commits July 23, 2026 22:41
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 Felipe705x changed the title typing/perf 8: interned type ids + meet/join memos (subq/optional −32%) typing/perf 4/5: schema-lifetime lookup structures — interning, memos, env ops, label index Aug 29, 2026
@Felipe705x
Felipe705x changed the base branch from tc-perf/07-env-costs to tc-perf/04-path-summary August 29, 2026 21:08
…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>
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