Skip to content

perf(jit): reduce native entry, feedback, and PC validation overhead - #18

Merged
huacnlee merged 5 commits into
mainfrom
perf/m3-native-entry-overhead
Sep 5, 2026
Merged

perf(jit): reduce native entry, feedback, and PC validation overhead#18
huacnlee merged 5 commits into
mainfrom
perf/m3-native-entry-overhead

Conversation

@huacnlee

@huacnlee huacnlee commented Sep 5, 2026

Copy link
Copy Markdown
Member

Issue # (if available)

Follow-up to M2 (#16).

Description of changes

Generic native calls repeatedly acquired and released entry handles and allocated return/argument feedback storage. Cache one idle pc=0 entry handle per QuickJS runtime, replace the native-return queue with a synchronous inline handoff, reuse CALL feedback storage after warmup, and resume helper PC validation from a previously verified instruction boundary.

  • Preserve exclusive active pins during recursion and GC; invalidate idle reuse on feedback/tier changes, demotion, retirement, suspension, and detach. Suppress cache population across reentrant release callbacks and preserve original function-generation attribution.
  • Add the optional pure entry_cache_epoch query, bump JIT ABI minor to 20, and update the vtable fingerprint, bundled bindings, and patch manifest. OSR handles remain uncached.
  • Preserve helper PC range/end/operand rejection with a private per-function boundary cursor; backward queries rescan, and invalidation resets the cursor. Add a boundary/ownership regression verified with a failing validation-bypass mutation.
  • Add native-acquisition metrics, a generic-call probe, ownership/invalidation regressions, and a thread-local allocation regression: 100 warmed 32-type CALL events previously allocated or reallocated 400 times and now allocate zero times.

The final cursor diagnostic records 1.356x M2 speed for generic calls in automatic mode (actually Tier 1; paired 95% interval: 35.3%–35.9% faster). Relative to the complete pre-cursor version, Tier 1 generic calls are 1.135x speed, direct calls 2.226x, and recursive Fibonacci 1.108x; automatic generic calls improve only to 1.007x. CALL-buffer reuse independently eliminates warmed allocations; its earlier incremental timing result was statistically tied.

There are small measured tradeoffs: focused 60-pair repeats retain automatic direct-call speed of 0.979x the pre-cursor baseline (1.3%–3.3% slower) and Tier 2 scalar-loop speed of 0.985x (1.1%–2.2% slower). Direct-call Tier 2 and automatic scalar-loop repeats are statistically tied. Reports preserve raw samples, exact executable/script hashes, the cursor patch, and reproduction drivers. The profiler's helper-frame share falls from 36.08% to 18.24% of self sampled cycles; that attribution is not an overall speed claim.

The pre-cursor clean-source matrix is archived with raw evidence: 22 workloads, four modes, and 2,640 retained samples. The overall performance acceptance gate still fails: Tier 2 compute speed is 3.17x–3.23x the interpreter baseline (below the 5x target), startup/reload gates fail, and host-integration evidence is absent. Checksum, required native-entry, automatic-policy, and aggregate P99 gates pass. That matrix predates the cursor change; it is not acceptance evidence for the final revision. Its mode comparisons are separate from the revision diagnostics above. Report labels now distinguish qualifying native entries for each requested tier. See docs/M3.md for evidence, ownership proofs, and limitations.

Validation on final code:

  • 512 release runtime tests: cargo test --release -p quickjs-jit-runtime --features compiler,test-support --tests
  • 55 helper, native-boundary and Tier 1 call tests under AddressSanitizer/LeakSanitizer on native Linux x86_64
  • Full-workspace Clippy with --features full-async,bindgen -- -D warnings; formatting check
  • 15 bindgen-enabled ABI tests, including exact bundled/fresh declaration comparison
  • 11 patch tests and 24 benchmark tests
  • Independent reviews of cache ownership/invalidation, return handoff, feedback reuse, and PC boundary reuse; identified cache issues fixed with reproducing regressions

CI for the cursor commit remains pending. Its parent b7be6a4 passed 41 checks, including memory, address, and thread sanitizers. Per-call hot/enter/exit bookkeeping, feedback/tier-state lookups, and remaining frame-validation work persist; this PR does not complete the broader native-call-overhead target.

Checklist

  • Added change to the changelog
  • Created unit tests for my feature if needed

@huacnlee huacnlee changed the title perf(jit): cache native entries and reuse call feedback storage perf(jit): reduce native entry, feedback, and PC validation overhead Sep 5, 2026
@huacnlee
huacnlee merged commit a38bd01 into main Sep 5, 2026
14 checks passed
@huacnlee
huacnlee deleted the perf/m3-native-entry-overhead branch September 5, 2026 12:18
huacnlee added a commit that referenced this pull request Sep 6, 2026
)

### Issue # (if available)

Follow-up to #18.

### Description of changes

Native entry timing used a hash map of per-function vectors, adding
lookups and allocations to callback bookkeeping. Replace it with a
shared LIFO stack while preserving the original function key and tier
through recursion, retirement, OSR, and all exit kinds. Empty or
mismatched exits preserve other active records. After warming one slot,
inserting timers for 100 sequential distinct functions now performs zero
allocations instead of 105.

The performance controls also exposed forced Tier 2 timing before its
caller finished compiling. Add an explicit publication requirement for
call-heavy, stable compilation settling, and rejection of compilation
during required-Tier2 timing. Refresh the changed script checksum and
three pre-existing stale local manifest hashes.

Archive profiles and four paired comparisons against merged M3 with
identical corrected harnesses and balanced engine order. Generic
automatic measures 1.011x baseline speed, with its interval only
narrowly excluding parity. Generic Tier 1 and recursive Tier 1 are
statistically tied with broad intervals; direct-call modes are also
tied. These dirty-source diagnostics do not establish overall speedup or
complete the broader clean-source performance acceptance gate.
Documentation retains the original readiness-defect evidence and
explains the superseded measurement limitations.

Validation:
- 515 release runtime tests passed.
- 87 runtime AddressSanitizer/LeakSanitizer tests passed.
- 28 benchmark tests and two forced-Tier2 sanitizer tests passed.
- Allocation and premature-readiness regressions were verified red
before the fixes.
- Full-workspace Clippy and formatting passed.
- Final measurement audit verified matching checksums, balanced native
enter/exit counts, executable/source hashes, and 15/15 latency plus 5/5
throughput first-run order in every mode.

### Checklist

- [x] Added change to the changelog
- [x] Created unit tests for my feature if needed
huacnlee added a commit that referenced this pull request Sep 6, 2026
### Issue # (if available)

Follow-up to merged #19; review this diff against `main`.

### Description of changes

Consolidate `observe_type` and `observe_call` into one B-tree entry
lookup, preserving occupied-at-capacity updates, rejected-entry
accounting, exact feedback versions, and independent call-slot growth.
Cache baseline tier state once after maintenance in `record_hot`;
successful direct refresh returns before that cached value is reused.

Add three characterization tests covering the capacity, generation,
version and megamorphic edges. Deliberate capacity and arity mutations
fail the corresponding tests. Validation: 518 release runtime tests, 82
targeted ASAN/LSAN tests, workspace Clippy and formatting pass;
independent code review found no issues.

### Measured performance

**Compared with `a38bd01` (merged #18), the cumulative #19 + #20 changes
improve generic-call Tier 1 speed by 19.3%, generic-call automatic by
18.1%, and recursive Tier 1 by 18.7%.** The measured candidate is
`99a1e78`; the conflict-resolution merge retains that exact content
tree. These are cumulative results, not the incremental gain of #20 over
the current `main` containing #19.

The values below are steady-state latency converted to speed relative to
the named baseline: **1.00x is parity; higher is faster**. Intervals are
paired 95% bootstrap confidence intervals.

| Workload | Requested mode / actual execution | Speed vs. `a38bd01` |
Speed improvement | 95% interval |
|---|---|---:|---:|---:|
| Generic calls | Tier 1 | 1.193x | 19.3% faster | 1.190x–1.197x |
| Generic calls | Tier 2 / actually Tier 1 | 1.163x | 16.3% faster |
1.160x–1.165x |
| Generic calls | Automatic / actually Tier 1 | 1.181x | 18.1% faster |
1.175x–1.185x |
| Recursive Fibonacci | Tier 1 | 1.187x | 18.7% faster | 1.183x–1.189x |
| Scalar loop | Tier 2 | 1.035x | 3.5% faster | 1.028x–1.038x |
| Scalar loop | Automatic | 1.041x | 4.1% faster | 1.037x–1.047x |

Controls and limitations:

- All direct-call modes, property-access modes, and interpreter
comparisons are **statistically tied**. For example, direct-call Tier 2
ranges from 1.9% slower to 0.2% faster; property automatic ranges from
0.24% slower to 0.15% faster.
- Scalar Tier 1 is tied, between 2.3% slower and 0.6% faster. Recursive
requested Tier 2 and automatic are also tied; automatic recursion has
zero native entries and remains an interpreter-path control.
- The scalar-loop improvements have no isolated causal attribution.
These measurements neither establish an overall no-regression result nor
replace the complete clean-source acceptance matrix.
- Later rooted-property-guard changes and their measurements are outside
this PR and are not included in this table.

Method: five discarded warmup processes, 30 fresh-process latency pairs
per mode, ten throughput windows of at least one second per engine/mode,
and 10,000 paired bootstrap resamples, pinned to CPU 0. Both runtimes
use the identical corrected benchmark harness. Engine order is balanced
independently of mode order (15/15 first positions for latency; 5/5 for
throughput). Checksums, native entry/exit balance, and executable/source
hashes were audited. The recorded runs use dirty-source diagnostic
provenance; throughput windows count fresh workers including setup and
warmup, not JavaScript operations.

Raw samples, exact driver/harness source, hashes, and intervals:
[generic
calls](https://github.com/longbridge/quickjs-jit/blob/99a1e78/benchmarks/results/m3-feedback-lookups-generic-call-entry-paired.json),
[direct
calls](https://github.com/longbridge/quickjs-jit/blob/99a1e78/benchmarks/results/m3-feedback-lookups-call-heavy-paired.json),
[recursive
Fibonacci](https://github.com/longbridge/quickjs-jit/blob/99a1e78/benchmarks/results/m3-feedback-lookups-fibonacci-recursive-paired.json),
[scalar
loop](https://github.com/longbridge/quickjs-jit/blob/99a1e78/benchmarks/results/m3-feedback-lookups-scalar-loop-paired.json),
[property
access](https://github.com/longbridge/quickjs-jit/blob/99a1e78/benchmarks/results/m3-feedback-lookups-property-heavy-paired.json).

Archive raw evidence and a property profile: helper-frame validation
remains the main sampled property cost. Document the next bounded
ownership optimization and its safety constraints; property lowering
itself is unchanged in this PR.

### Checklist

- [x] Added change to the changelog
- [x] Created unit tests for my feature if needed
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