Skip to content

Emit backend: three measurement-gated perf micro-optimizations (D1 / C3 / B5) #54

Closed as not planned

Description

@johnsoncodehk

Follow-up from the #45 emit-backend gap audit. Three perf micro-optimizations were verified as real opportunities but are NEEDS_MEASUREMENT: every benchmark in the repo (test/perf-bench.ts, test/emit-parser-bench.ts, test/bench.ts, test/head-to-head.ts) reads the pinned /tmp/ts-repo corpus, so the project's measure-first perf discipline can't be satisfied without it. Each should be implemented byte-identical (proven by the now-CI-wired parity gates) and landed only if the corpus bench shows a win.

D1 — inline alt has no predictive dispatch

matchInto's case 'alt' (an alt inside a seq) emits a naive shared-save try-each-arm, while rule-level longest-match already has altMaskDispatch (bitmask) + SECOND refinement. FIRST-disjoint inline alts could switch-dispatch on the first token (ordered first-match is preserved when arms are FIRST-disjoint); 2-arm alts could pick the arm from the first token with no save/restore. Scale: typescript.ts ~78 alt(, javascript.ts ~43. Most substantive of the three.

C3 — SURG_ELEM table is too narrow

The node-surgery element table is built only for a pure seq/group around exactly one */+ with no alt/opt/?/multiple reps, so any list rule with an optional element or alternative (trailing comma, member list with modifiers, union body) gets no surgery and every edit inside it takes a full adoption re-parse. The audit suggests opts around the rep region may be admissible via the rowKC watermark argument — but the agent flagged uncertainty about whether the watermark check handles a [rep*, optional] kid structure, so this is the riskiest of the three and needs the surgery layout invariants worked out + exhaustive-edits confirmation (with the relaxed path asserted to fire).

B5 — lexKwT call-site length sentinel

The identifier hot path scans [pos+1, p) in the char loop, then lexKwT re-checks length-bounded keyword candidates. A top-level LX_MAXKW constant could short-circuit the call ((p - pos) > LX_MAXKW ? 0 : lexKwT(...)). Caveat found during the audit: lexKwT already bails on its first statement (if (n < minKw || n > maxKw) return 0), so the only saving is the function-call overhead for long identifiers — whether that helps depends on whether V8 already inlines lexKwT (it has a large switch). Smallest expected win; measure before bothering.

All three were left out of #45 / PR #53 precisely because landing an unmeasured perf change would violate the repo's measure-first rule, not because they aren't worth doing.

Activity

  1. johnsoncodehk commented on Jun 20, 2026

    @johnsoncodehk
    OwnerAuthor

    Note discovered while wiring the full-corpus emit-parity job (PR #53): D1 also has a correctness/parity payoff, not just perf. The emitter's naive inline-alt try-each-arm explores an arm the interpreter prunes, so on rare rejected inputs (e.g. const { 0n: f } = arr;) the two engines reach a different farthest-exploration token — identical CST and identical primary error, but a different [farthest: …] hint. PR #53 handles this by pinning the primary error and treating the hint as a benign Layer-B artifact. Giving the emitter the interpreter's inline-alt dispatch (D1) would prune the same arm and restore exact [farthest: …] parity as a side effect.

  2. johnsoncodehk commented on Jun 20, 2026

    @johnsoncodehk
    OwnerAuthor

    Measured all three against the pinned TS corpus (now that the corpus blocker is gone). The emitted parser is already ~2.3 ms / ~14× the interpreter on the bench files, and none of the three justifies landing.

    B5 — lexKwT call-site length sentinel. lexKwT is 3.3% self-time in the emit profile, but the sentinel only skips the call for runs outside [minKw, maxKw], and lexKwT already bails on its first statement (if (n < minKw || n > maxKw) return 0). Tokenize-only A/B (best-of-7, N=200): 1.224 ms with the sentinel vs 1.226 ms without — no measurable change. V8 evidently handles the early-exit efficiently, exactly the caveat noted when filing. Byte-identical, but zero gain → not landed.

    D1 — inline-alt predictive dispatch. Implemented a FIRST-guard on every seq-internal alt arm (recovering || _q(save), byte-identical incl. maxPos — a skipped arm fails at its first token without advancing; 869 guards emitted; full-corpus CST parity held). Same-process interleaved A/B (best-of-8 rounds × 6): baseline 2.312 ms vs D1 2.300 ms = 0.5% faster (noise floor), and the emitted parser grows +3% (+35 KB). The profile explains it: inline-alt FIRST-dispatch (_q) is 0.02% and the arm matchers <1% each — not a hotspot. A 3% bigger artifact for a noise-floor speedup isn't worth it → not landed.

    C3 — SURG_ELEM relaxation (sep-list surgery). The list-shaped rules excluded from surgery are the sep-based comma-lists (call args, array/object literals, params). Measured edit latency vs list size:

    • array-literal (sep of Expr): 0.039 ms @50 → 0.046 @200 → 0.145 @800 — O(N) adoption.
    • union-type (sep of Type): flat ~0.01 ms — already fast (routes through the Pratt/memo path, not adoption).
    • block many(Stmt) (surgery-eligible): flat ~0.007 ms — the O(1) baseline.

    So the win is real only for large Expr comma-lists, and sub-millisecond except for pathologically large (generated) literals. Capturing it needs sep-aware surgery — a splice that handles the interleaved delimiters — which directly contradicts the engine's documented soundness invariant for surgery (the SURG_ELEM build comment: "a longest-match arm (or lookahead) at the container's OWN level may probe into the rep zone without any kid row owning the read, which would break the prefix-keep watermark argument"). That's a high-risk rewrite of the most delicate code for a marginal, rarely-hit gain.

    Closing as a measured wontfix: the engine is already highly optimized, and these three micro-optimizations are either zero-gain (B5), noise-floor-for-bigger-artifact (D1), or marginal-gain-against-a-soundness-invariant (C3). Reopen if large-literal incremental latency becomes a real product concern — C3's path is the one with a (modest) payoff, gated by exhaustive-edits as the soundness arbiter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions