perf(runtime): positive-cache the box-pointer registry probe and inline the runtime-handle accessors - #7906
Conversation
…ne the runtime-handle accessors The async-to-generator transform boxes every body local of an `async` function, and `js_box_get`/`js_box_set` validate their operand against a thread-local hash set on every access (perry#4898). On a promise-only kernel (24 000 activations, 48 000 awaits) the three `is_registered_*_box_ptr` probes were 8.2 % + 5.9 % + 5.5 % of leaf samples — the largest single item in Perry's async machinery. Front each registry with an 8-slot direct-mapped POSITIVE cache. Sound because the registries are monotonic: boxes are never freed and nothing ever removes an entry, so "this address is a registered box" cannot become false. Only confirmed positives are recorded; a miss still falls through to the authoritative hash set, so an unregistered-but-plausible address is rejected exactly as strictly as before. Also make the `RuntimeHandle` accessors and `RuntimeHandleScope` root helpers inlineable: their formatted `expect`/`panic!` arms were most of each function's estimated size and kept them out of line in the release build (`get_nanbox_u64` 4.3 %, `root_nanbox_f64` 3.0 % of leaf samples on an async-heavy program, purely as call frames). The panics move to `#[cold]` out-of-line helpers.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe runtime adds positive caches for three box registries and improves runtime-handle inlining. Failure paths now use cold panic helpers. Tests cover cache collisions, eviction, kind isolation, and reset cleanup. ChangesRuntime optimizations
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
MeasurementTwo runtime archives from the same worktree (pre-/post-change), identical compiler source,
Child CPU time agreed in direction on the target (−4.8 %, best of 11, noise floor ~1 %). The gap between the target (−2.4 %) and the pure kernels (−4.4 to −7.2 %) is expected No regression19/19 corpus programs byte-identical to
|
Two contained runtime changes on the hot path of
async/await, found by profilinggc-handoff/apps/asyncpipe.ts(currently 1.63× node) for the first time.Why
The async-to-generator transform boxes every body local of an
asyncfunction, andjs_box_get/js_box_setvalidate their operand against a thread-local hash set onevery access (perry#4898). On a promise-only kernel — 24 000 activations, 48 000 awaits,
no objects/strings/Map — the three registry probes are the largest single item in
Perry's async machinery:
is_registered_box_ptris_registered_i32_box_ptris_registered_bool_box_ptrRuntimeHandle::get_nanbox_u64RuntimeHandleScope::root_nanbox_f64RuntimeHandle::get_raw_mut_ptrThe state machine re-reads the same handful of boxes (
__gen_state,__gen_done,__gen_executing, plus the activation's locals) on every step, so the probe is almostalways answering "yes" about an address it answered "yes" about a moment ago.
What
1. An 8-slot direct-mapped positive cache per box registry.
Sound because the registries are monotonic:
js_box_alloc*inserts and nothing everremoves — boxes are never freed — so "this address is a registered box" can never
become false, and an address can never be recycled into a non-box allocation. A cache hit
is therefore exactly as authoritative as the hash probe it replaces.
A negative cache would not be sound (an address that is not a box today can be minted
as one tomorrow), so only confirmed positives are recorded and every miss still falls
through to the hash set. The perry#4898 rejection — a read-only
__TEXT.__cstringaddress that passes every structural check — is unchanged.
test_clear_box_registry(test-only, and the one operation that breaks monotonicity)clears the caches with the registries.
2. Make the
RuntimeHandleaccessors inlineable.Their formatted
expect/panic!arms were most of each function's estimated size andkept them out of line in the release build, so a promise-heavy program paid a real call
frame per rooted-value read. The panics move to
#[cold] #[inline(never)]helpers and theaccessors get
#[inline]. No semantic change.Tests
box_ptr_cache_rejects_a_colliding_unregistered_address— warms the cache with a realbox, then probes plausible-but-unregistered addresses that map to the same cache
slot. Sabotage-verified: changing the hit test from a full-address compare to a
slot-occupancy compare makes it fail with
a colliding unregistered address must still be rejected.box_ptr_cache_eviction_does_not_lose_a_real_box— the cache is an accelerator, neverthe source of truth.
box_ptr_caches_do_not_cross_kinds— an ordinary box is not accepted as an i32/bool box.Measurement
Numbers in the PR comments below and in
gc-handoff/ASYNC2-NOTES.md. The dev box ran atload 25–220 all session, so the A/B is by instructions retired (
/usr/bin/time -l,load-independent, run-to-run spread ≈0.2 %) with child CPU time alongside; authoritative
wall-clock numbers come from the quiet mini, which the main session owns.
What this PR is not
Profiling
asyncpipeturned up two larger levers that are not in this PR, bothwritten up with measurements in
gc-handoff/ASYNC2-NOTES.md:asyncpipeis incremental-GC work in a program that runs zero GCcycles (
PERRY_GC_MOVING_LOOP_POLLS=0measures −14.1 %, still zero cycles). GCworkstream.
Get(resolution, "then")probe on every object anasyncfunctionresolves with — re-interning the
"then"key, asetjmpframe, and a full name-keyedmiss walk into
Object.prototype, 72 000 times, answeringundefinedevery time. Thesafe fix needs an
Object.prototypemutation-generation counter; getting that wrongmakes a real thenable stop being assimilated, and the failure mode is a hang.
Summary by CodeRabbit
Performance
Reliability
Documentation