Skip to content

Commit d1873a3

Browse files
proggeramlugRalph Küpper
andauthored
perf(codegen): #6804 — PIC compares the discriminated shape token + slot bounds check (#6808)
* perf(codegen,runtime): #6804 — PIC compares the discriminated shape token; slot bounds check The per-site PIC token is now: header-stamped ShapeId lifted above the 48-bit pointer space (bit 62) for stamped plain objects, keys pointer for class instances / unstamped receivers — one compare, no discriminant word, the token kinds cannot collide numerically. Id tokens are immune to address-recycling ABA, so owned keys arrays are safely PIC-cacheable again for plain objects. The hit path also bounds the cached slot by the receiver's inline capacity, closing a latent over-read for same-shape siblings with different physical allocations. * docs(changelog): changeset for #6808 --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent e4fbb19 commit d1873a3

4 files changed

Lines changed: 88 additions & 26 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
perf(codegen): #6804 second half — the generic property-get PIC compares a discriminated shape token: header-stamped ShapeIds (lifted above the 48-bit pointer space) for plain objects — stable across grow-reallocs and GC moves, immune to address-recycling, and re-enabling PIC caching for owned keys arrays — with keys-pointer tokens retained for class instances. The hit path also bounds the cached slot by the receiver's inline capacity, closing a latent over-read for same-shape siblings with different physical allocations.

crates/perry-codegen/src/expr/property_get/generic_dispatch.rs

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -327,23 +327,47 @@ pub(crate) fn lower_generic_property_get(
327327
let keys_ptr_p = ctx.block().inttoptr(I64, &keys_addr);
328328
let keys_val = ctx.block().load(I64, &keys_ptr_p);
329329

330-
// Load cached keys_array from the per-site global.
330+
// #6804: the receiver's shape TOKEN. A plain object stamped with a
331+
// runtime ShapeId (`parent_class_id` ∈ [0x8000_0000, 0xC000_0000) —
332+
// see `shapes::SHAPE_ID_BASE/END`; a real parent class id can never
333+
// fall in that range) compares by id: stable across keys grow-reallocs
334+
// and GC moves, and immune to address recycling (ids are never
335+
// reused). Everything else (class instances, unstamped receivers)
336+
// keeps the keys-pointer compare. Id tokens are lifted above the
337+
// 48-bit pointer space (bit 62, `shapes::PIC_ID_TOKEN_BIT`) so the
338+
// two token kinds can never collide numerically — one compare, no
339+
// discriminant word. `parent_class_id` is a u32 at offset 8 on every
340+
// target (the four leading u32s precede the pointer fields).
341+
let pcid_addr = ctx.block().add(I64, &safe_obj_handle, "8");
342+
let pcid_ptr = ctx.block().inttoptr(I64, &pcid_addr);
343+
let pcid = ctx.block().load(I32, &pcid_ptr);
344+
// In-range test via wrapping add + ult: (pcid - 0x8000_0000) < 0x4000_0000.
345+
// (-2147483648 is the i32 spelling of the 0x8000_0000 subtrahend.)
346+
let pcid_rel = ctx.block().add(I32, &pcid, "-2147483648");
347+
let is_stamp = ctx.block().icmp_ult(I32, &pcid_rel, "1073741824");
348+
let pcid64 = ctx.block().zext(I32, &pcid, I64);
349+
// PIC_ID_TOKEN_BIT = 1 << 62.
350+
let id_token = ctx.block().or(I64, &pcid64, "4611686018427387904");
351+
let token = ctx.block().select(I1, &is_stamp, I64, &id_token, &keys_val);
352+
353+
// Load the cached token from the per-site global.
331354
let cache_keys_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "0")]);
332-
let cached_keys = ctx.block().load(I64, &cache_keys_ptr);
333-
let keys_eq = ctx.block().icmp_eq(I64, &keys_val, &cached_keys);
355+
let cached_token = ctx.block().load(I64, &cache_keys_ptr);
356+
let token_eq = ctx.block().icmp_eq(I64, &token, &cached_token);
334357
// #809: an object with `keys_array == null` (e.g. an
335358
// `Object.create(proto)` result, or any object with no own
336359
// string props) has no cacheable own-slot. The per-site cache
337-
// global is zero-initialized, so `keys_val (0) == cached_keys
338-
// (0)` spuriously "hits" and the hit path returns the empty
339-
// slot[0] — never invoking the miss handler, so the runtime's
340-
// prototype-chain walk in `js_object_get_field_by_name` is
341-
// skipped and `Object.create(P).m()` reads `undefined`. Require
342-
// a non-null keys_array for a hit so keyless receivers fall to
343-
// the slow path (which resolves inherited props correctly).
344-
let keys_nonnull = ctx.block().icmp_ne(I64, &keys_val, "0");
345-
let hit_keys = ctx.block().and(I1, &is_object, &keys_eq);
346-
let hit = ctx.block().and(I1, &hit_keys, &keys_nonnull);
360+
// global is zero-initialized, so a zero token would spuriously
361+
// "hit" and the hit path would return the empty slot[0] — never
362+
// invoking the miss handler, so the runtime's prototype-chain
363+
// walk in `js_object_get_field_by_name` is skipped and
364+
// `Object.create(P).m()` reads `undefined`. Require a non-zero
365+
// token for a hit so keyless receivers fall to the slow path
366+
// (which resolves inherited props correctly). Id tokens always
367+
// carry bit 62, so they are never zero.
368+
let token_nonnull = ctx.block().icmp_ne(I64, &token, "0");
369+
let hit_token = ctx.block().and(I1, &is_object, &token_eq);
370+
let hit = ctx.block().and(I1, &hit_token, &token_nonnull);
347371

348372
let hit_idx = ctx.new_block("pic.hit");
349373
let miss_idx = ctx.new_block("pic.miss");
@@ -353,14 +377,36 @@ pub(crate) fn lower_generic_property_get(
353377
let merge_label = ctx.block_label(merge_idx);
354378
ctx.block().cond_br(&hit, &hit_label, &miss_label);
355379

356-
// PIC hit: direct field load.
380+
// PIC hit: bounds-check the cached slot, then direct field load.
357381
ctx.current_block = hit_idx;
382+
let cache_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]);
383+
let slot = ctx.block().load(I64, &cache_slot_ptr);
384+
// #6804: bound the cached slot by THIS receiver's inline capacity.
385+
// Same-shape siblings can differ in physical allocation (an object
386+
// built at a small alloc site adopts a shared keys array whose later
387+
// slots live in its OVERFLOW map) — a slot primed from a
388+
// larger-capacity sibling must not drive a raw load past this
389+
// receiver's field region. `alloc_limit = max(field_count,
390+
// INLINE_SLOT_FLOOR=4)` mirrors the miss handler's cacheability
391+
// rule; an out-of-bounds slot falls to the miss path, which reads
392+
// the overflow map correctly (and records the guard failure —
393+
// `record_guard_pass` only fires after the bounds check passes).
394+
let fc_addr = ctx.block().add(I64, &safe_obj_handle, "12");
395+
let fc_ptr = ctx.block().inttoptr(I64, &fc_addr);
396+
let fc = ctx.block().load(I32, &fc_ptr);
397+
let fc64 = ctx.block().zext(I32, &fc, I64);
398+
let fc_floor = ctx.block().icmp_ult(I64, &fc64, "4"); // INLINE_SLOT_FLOOR
399+
let limit = ctx.block().select(I1, &fc_floor, I64, "4", &fc64);
400+
let slot_in_bounds = ctx.block().icmp_ult(I64, &slot, &limit);
401+
let bounds_hit = ctx.new_block("pic.hit.load");
402+
let bounds_hit_label = ctx.block_label(bounds_hit);
403+
ctx.block()
404+
.cond_br(&slot_in_bounds, &bounds_hit_label, &miss_label);
405+
ctx.current_block = bounds_hit;
358406
ctx.block().call_void(
359407
"js_typed_feedback_record_guard_pass",
360408
&[(I64, &feedback_site_id)],
361409
);
362-
let cache_slot_ptr = ctx.block().gep(I64, &cache_ref, &[(I64, "1")]);
363-
let slot = ctx.block().load(I64, &cache_slot_ptr);
364410
let offset = ctx.block().shl(I64, &slot, "3");
365411
// arm64_32 watchOS: the object fields region begins at
366412
// `size_of::<ObjectHeader>()` past the user pointer — 24 on 64-bit, 20 on

crates/perry-runtime/src/object/field_get_set/ic_miss.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -459,16 +459,24 @@ pub extern "C" fn js_object_get_field_ic_miss(
459459
// ~900k times per run (40% inclusive samples per
460460
// perfcomp.profile).
461461
//
462-
// #6759 C3c: only prime the cache for SHAPE-SHARED keys
463-
// arrays (literal shapes, class-keys arrays — both
464-
// shape-cache-resident, process-rooted, address-stable).
465-
// An OWNED keys array can die and have its address
466-
// recycled under a DIFFERENT shape, and this compare is
467-
// the one unvalidated fast path in the system — a stale
468-
// hit reads the wrong slot. Owned/wide receivers are
469-
// served by the validated, shape-id-keyed FIELD_CACHE
470-
// instead.
471-
if keys_cacheable_for_pic(keys) {
462+
// #6804: a stamped plain receiver primes an ID token
463+
// (`stamp | PIC_ID_TOKEN_BIT`, matching the emitted
464+
// PIC's discriminated compare). Ids are never reused,
465+
// so id tokens are immune to the address-recycling ABA
466+
// that keys-pointer tokens have — which also makes
467+
// OWNED keys arrays safely cacheable again for plain
468+
// objects. #6759 C3c: keys-POINTER tokens stay
469+
// restricted to SHAPE-SHARED arrays (literal shapes,
470+
// class-keys arrays — shape-cache-resident,
471+
// process-rooted, address-stable), because that compare
472+
// is unvalidated and a recycled owned-array address
473+
// would read the wrong slot.
474+
let stamp = (*obj).parent_class_id;
475+
if (*obj).class_id == 0 && crate::object::shapes::is_shape_id(stamp) {
476+
(*cache)[0] =
477+
(stamp as u64 | crate::object::shapes::PIC_ID_TOKEN_BIT) as i64;
478+
(*cache)[1] = i as i64;
479+
} else if keys_cacheable_for_pic(keys) {
472480
(*cache)[0] = keys as i64;
473481
(*cache)[1] = i as i64;
474482
}

crates/perry-runtime/src/object/shapes.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ pub(crate) fn is_shape_id_token(v: usize) -> bool {
8383
v >= SHAPE_ID_BASE as usize && v < SHAPE_ID_END as usize
8484
}
8585

86+
/// #6804: lifts a ShapeId into the per-site PIC token space, ABOVE the
87+
/// 48-bit pointer range, so an id token can never numerically equal a
88+
/// keys-array pointer token. MUST match the literal the PIC IR emits in
89+
/// `perry-codegen/src/expr/property_get/generic_dispatch.rs`
90+
/// (4611686018427387904 = 1 << 62).
91+
pub(crate) const PIC_ID_TOKEN_BIT: u64 = 1 << 62;
92+
8693
fn alloc_shape_id() -> u32 {
8794
use std::sync::atomic::Ordering;
8895
let id = SHAPE_ID_NEXT.fetch_add(1, Ordering::Relaxed);

0 commit comments

Comments
 (0)