-
-
Notifications
You must be signed in to change notification settings - Fork 158
perf: stop re-entering the runtime to append to and pop from an Array subclass's own elements store (−9.5% / −11.4%) #8985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70ef089
9e752b6
cf263d2
255aebd
b071bbc
9dc9933
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| Private class fields now preserve their value under compound and logical | ||
| assignments instead of reading `undefined` and storing `NaN`. | ||
|
|
||
| Private fields no longer occupy public class-shape keys, so they stay absent | ||
| from `Object.keys`, `Object.getOwnPropertyNames`, `for...in`, spread, and JSON | ||
| serialization. An ordinary property whose name matches Perry's transient | ||
| private-member routing spelling is now retained as ordinary user data. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| ### Changed | ||
|
|
||
| - Appending to and popping from a `class X extends Array` instance no longer re-classifies its own elements store: an in-capacity append and a non-hole tail pop run without a handle scope, a proxy probe, forwarding-stub cleaning or flag resolution, keeping only the element bookkeeping the read tiers and the loop guard consume. Growth, holes, an empty store and every exotic flag keep the complete runtime entry. | ||
| - `sub.pop()` on such an instance now pops inline as well: the codegen tier resolves the payload through the meta record and runs the same length/read/take blocks on it, instead of calling the runtime entry that only re-derives the store. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,19 +36,30 @@ const POINTER_TAG_HI16: &str = "32765"; // 0x7FFD | |||||||||||||||||||||||||||||||||||||||
| const HANDLE_BAND_TOP: &str = "1048575"; // 0x0FFFFF — heap objects are above | ||||||||||||||||||||||||||||||||||||||||
| const HEAP_LIMIT: &str = "140737488355328"; // 2^47 | ||||||||||||||||||||||||||||||||||||||||
| const GC_TYPE_ARRAY_I8: &str = "1"; | ||||||||||||||||||||||||||||||||||||||||
| const GC_TYPE_OBJECT_I8: &str = "2"; | ||||||||||||||||||||||||||||||||||||||||
| const GC_FLAG_FORWARDED_I8: &str = "-128"; // 0x80 as i8 | ||||||||||||||||||||||||||||||||||||||||
| const MAX_FAST_LENGTH_I32: &str = "100000000"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Lower `recv.pop()` for an Array-admitted receiver: the inline tier above | ||||||||||||||||||||||||||||||||||||||||
| /// with `js_array_pop_f64` behind it. Returns the popped element (boxed). | ||||||||||||||||||||||||||||||||||||||||
| pub(crate) fn lower_array_pop_inline(ctx: &mut FnCtx<'_>, recv_box: &str) -> String { | ||||||||||||||||||||||||||||||||||||||||
| let meta_offset = | ||||||||||||||||||||||||||||||||||||||||
| crate::target_layout::object_meta_slot_offset_bytes(ctx.target_triple).to_string(); | ||||||||||||||||||||||||||||||||||||||||
| let hdr_idx = ctx.new_block("apop.hdr"); | ||||||||||||||||||||||||||||||||||||||||
| // An elements-backed Array subclass (`ObjectMeta.elements`, word 12) pops | ||||||||||||||||||||||||||||||||||||||||
| // from its store: the header gate resolves the payload and the same | ||||||||||||||||||||||||||||||||||||||||
| // length/read/take blocks run on it, so `sub.pop()` stops paying a runtime | ||||||||||||||||||||||||||||||||||||||||
| // entry that only re-derives the store (5.1% of the wolf-ecs entity cycle). | ||||||||||||||||||||||||||||||||||||||||
| let elem_idx = ctx.new_block("apop.elements"); | ||||||||||||||||||||||||||||||||||||||||
| let elem_check_idx = ctx.new_block("apop.elements.check"); | ||||||||||||||||||||||||||||||||||||||||
| let len_idx = ctx.new_block("apop.len"); | ||||||||||||||||||||||||||||||||||||||||
| let read_idx = ctx.new_block("apop.read"); | ||||||||||||||||||||||||||||||||||||||||
| let take_idx = ctx.new_block("apop.take"); | ||||||||||||||||||||||||||||||||||||||||
| let slow_idx = ctx.new_block("apop.slow"); | ||||||||||||||||||||||||||||||||||||||||
| let merge_idx = ctx.new_block("apop.merge"); | ||||||||||||||||||||||||||||||||||||||||
| let hdr_label = ctx.block_label(hdr_idx); | ||||||||||||||||||||||||||||||||||||||||
| let elem_label = ctx.block_label(elem_idx); | ||||||||||||||||||||||||||||||||||||||||
| let elem_check_label = ctx.block_label(elem_check_idx); | ||||||||||||||||||||||||||||||||||||||||
| let len_label = ctx.block_label(len_idx); | ||||||||||||||||||||||||||||||||||||||||
| let read_label = ctx.block_label(read_idx); | ||||||||||||||||||||||||||||||||||||||||
| let take_label = ctx.block_label(take_idx); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -90,18 +101,72 @@ pub(crate) fn lower_array_pop_inline(ctx: &mut FnCtx<'_>, recv_box: &str) -> Str | |||||||||||||||||||||||||||||||||||||||
| let plain = blk.icmp_eq(I16, &blocking, "0"); | ||||||||||||||||||||||||||||||||||||||||
| let invalidated = blk.load_volatile(I8, "@PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED"); | ||||||||||||||||||||||||||||||||||||||||
| let prototype_clean = blk.icmp_eq(I8, &invalidated, "0"); | ||||||||||||||||||||||||||||||||||||||||
| let mut ok = blk.and(I1, ¬_fwd, &plain); | ||||||||||||||||||||||||||||||||||||||||
| ok = blk.and(I1, &ok, &prototype_clean); | ||||||||||||||||||||||||||||||||||||||||
| let array_ok = blk.and(I1, &ok, &is_array); | ||||||||||||||||||||||||||||||||||||||||
| // A `GC_TYPE_OBJECT` receiver may be an elements-backed Array | ||||||||||||||||||||||||||||||||||||||||
| // subclass; anything else keeps the runtime entry. | ||||||||||||||||||||||||||||||||||||||||
| let is_object = blk.icmp_eq(I8, &gc_type, GC_TYPE_OBJECT_I8); | ||||||||||||||||||||||||||||||||||||||||
| blk.cond_br(&array_ok, &len_label, &elem_label); | ||||||||||||||||||||||||||||||||||||||||
| let _ = is_object; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| let hdr_end = ctx.block_label(hdr_idx); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ctx.current_block = elem_idx; | ||||||||||||||||||||||||||||||||||||||||
| let store = { | ||||||||||||||||||||||||||||||||||||||||
| let blk = ctx.block(); | ||||||||||||||||||||||||||||||||||||||||
| let gc_type_addr = blk.sub(I64, &handle, "8"); | ||||||||||||||||||||||||||||||||||||||||
| let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); | ||||||||||||||||||||||||||||||||||||||||
| let gc_type = blk.load(I8, &gc_type_ptr); | ||||||||||||||||||||||||||||||||||||||||
| let is_object = blk.icmp_eq(I8, &gc_type, GC_TYPE_OBJECT_I8); | ||||||||||||||||||||||||||||||||||||||||
| let meta_addr = blk.add(I64, &handle, &meta_offset); | ||||||||||||||||||||||||||||||||||||||||
| let meta_slot = blk.inttoptr(I64, &meta_addr); | ||||||||||||||||||||||||||||||||||||||||
| let meta = blk.load(I64, &meta_slot); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Load the metadata pointer at the target pointer width.
Load Proposed fix+ let meta_ptr_size = if crate::target_layout::target_is_ilp32(ctx.target_triple) {
+ 4
+ } else {
+ 8
+ };
let store = {
let blk = ctx.block();
// ...
- let meta = blk.load(I64, &meta_slot);
+ let meta_native = blk.load(
+ if meta_ptr_size == 4 { I32 } else { I64 },
+ &meta_slot,
+ );
+ let meta = if meta_ptr_size == 4 {
+ blk.zext(I32, &meta_native, I64)
+ } else {
+ meta_native
+ };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| let has_meta = blk.icmp_ne(I64, &meta, "0"); | ||||||||||||||||||||||||||||||||||||||||
| let can_read_meta = blk.and(I1, &is_object, &has_meta); | ||||||||||||||||||||||||||||||||||||||||
| // `select` keeps the load of word 12 off a null meta pointer. | ||||||||||||||||||||||||||||||||||||||||
| let safe_meta = blk.select(I1, &can_read_meta, I64, &meta, &handle); | ||||||||||||||||||||||||||||||||||||||||
| let meta_ptr = blk.inttoptr(I64, &safe_meta); | ||||||||||||||||||||||||||||||||||||||||
| let store_slot = blk.gep(I64, &meta_ptr, &[(I64, "12")]); | ||||||||||||||||||||||||||||||||||||||||
| let store = blk.load(I64, &store_slot); | ||||||||||||||||||||||||||||||||||||||||
| let has_store = blk.icmp_ne(I64, &store, "0"); | ||||||||||||||||||||||||||||||||||||||||
| let ok = blk.and(I1, &can_read_meta, &has_store); | ||||||||||||||||||||||||||||||||||||||||
| blk.cond_br(&ok, &elem_check_label, &slow_label); | ||||||||||||||||||||||||||||||||||||||||
| store | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ctx.current_block = elem_check_idx; | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| let blk = ctx.block(); | ||||||||||||||||||||||||||||||||||||||||
| let gc_type_addr = blk.sub(I64, &store, "8"); | ||||||||||||||||||||||||||||||||||||||||
| let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); | ||||||||||||||||||||||||||||||||||||||||
| let gc_type = blk.load(I8, &gc_type_ptr); | ||||||||||||||||||||||||||||||||||||||||
| let is_array = blk.icmp_eq(I8, &gc_type, GC_TYPE_ARRAY_I8); | ||||||||||||||||||||||||||||||||||||||||
| let gc_flags_addr = blk.sub(I64, &store, "7"); | ||||||||||||||||||||||||||||||||||||||||
| let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr); | ||||||||||||||||||||||||||||||||||||||||
| let gc_flags = blk.load(I8, &gc_flags_ptr); | ||||||||||||||||||||||||||||||||||||||||
| let fwd_bits = blk.and(I8, &gc_flags, GC_FLAG_FORWARDED_I8); | ||||||||||||||||||||||||||||||||||||||||
| let not_fwd = blk.icmp_eq(I8, &fwd_bits, "0"); | ||||||||||||||||||||||||||||||||||||||||
| let reserved_addr = blk.sub(I64, &store, "6"); | ||||||||||||||||||||||||||||||||||||||||
| let reserved_ptr = blk.inttoptr(I64, &reserved_addr); | ||||||||||||||||||||||||||||||||||||||||
| let reserved = blk.load(I16, &reserved_ptr); | ||||||||||||||||||||||||||||||||||||||||
| let blocking = blk.and(I16, &reserved, POP_BLOCKING_FLAGS_I16); | ||||||||||||||||||||||||||||||||||||||||
| let plain = blk.icmp_eq(I16, &blocking, "0"); | ||||||||||||||||||||||||||||||||||||||||
| let mut ok = blk.and(I1, &is_array, ¬_fwd); | ||||||||||||||||||||||||||||||||||||||||
| ok = blk.and(I1, &ok, &plain); | ||||||||||||||||||||||||||||||||||||||||
| ok = blk.and(I1, &ok, &prototype_clean); | ||||||||||||||||||||||||||||||||||||||||
| blk.cond_br(&ok, &len_label, &slow_label); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| let elem_end = ctx.block_label(elem_check_idx); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| ctx.current_block = len_idx; | ||||||||||||||||||||||||||||||||||||||||
| let payload = ctx | ||||||||||||||||||||||||||||||||||||||||
| .block() | ||||||||||||||||||||||||||||||||||||||||
| .phi(I64, &[(&handle, &hdr_end), (&store, &elem_end)]); | ||||||||||||||||||||||||||||||||||||||||
| let new_length = { | ||||||||||||||||||||||||||||||||||||||||
| let blk = ctx.block(); | ||||||||||||||||||||||||||||||||||||||||
| // ArrayHeader: length @0 (i32), capacity @4 (i32), elements @8. | ||||||||||||||||||||||||||||||||||||||||
| let length = blk.safe_load_i32_from_ptr(&handle); | ||||||||||||||||||||||||||||||||||||||||
| let cap_addr = blk.add(I64, &handle, "4"); | ||||||||||||||||||||||||||||||||||||||||
| let length = blk.safe_load_i32_from_ptr(&payload); | ||||||||||||||||||||||||||||||||||||||||
| let cap_addr = blk.add(I64, &payload, "4"); | ||||||||||||||||||||||||||||||||||||||||
| let cap_ptr = blk.inttoptr(I64, &cap_addr); | ||||||||||||||||||||||||||||||||||||||||
| let capacity = blk.load(I32, &cap_ptr); | ||||||||||||||||||||||||||||||||||||||||
| let nonempty = blk.icmp_ne(I32, &length, "0"); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -119,7 +184,7 @@ pub(crate) fn lower_array_pop_inline(ctx: &mut FnCtx<'_>, recv_box: &str) -> Str | |||||||||||||||||||||||||||||||||||||||
| let blk = ctx.block(); | ||||||||||||||||||||||||||||||||||||||||
| let new_length_i64 = blk.zext(I32, &new_length, I64); | ||||||||||||||||||||||||||||||||||||||||
| let elem_off = blk.shl(I64, &new_length_i64, "3"); | ||||||||||||||||||||||||||||||||||||||||
| let elements_addr = blk.add(I64, &handle, "8"); | ||||||||||||||||||||||||||||||||||||||||
| let elements_addr = blk.add(I64, &payload, "8"); | ||||||||||||||||||||||||||||||||||||||||
| let elem_addr = blk.add(I64, &elements_addr, &elem_off); | ||||||||||||||||||||||||||||||||||||||||
| let elem_ptr = blk.inttoptr(I64, &elem_addr); | ||||||||||||||||||||||||||||||||||||||||
| let elem = blk.load(DOUBLE, &elem_ptr); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -132,7 +197,7 @@ pub(crate) fn lower_array_pop_inline(ctx: &mut FnCtx<'_>, recv_box: &str) -> Str | |||||||||||||||||||||||||||||||||||||||
| ctx.current_block = take_idx; | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| let blk = ctx.block(); | ||||||||||||||||||||||||||||||||||||||||
| let len_ptr = blk.inttoptr(I64, &handle); | ||||||||||||||||||||||||||||||||||||||||
| let len_ptr = blk.inttoptr(I64, &payload); | ||||||||||||||||||||||||||||||||||||||||
| // `length` is a plain i32 word: no pointer, no barrier, no layout | ||||||||||||||||||||||||||||||||||||||||
| // note — exactly the runtime fast path's single store. | ||||||||||||||||||||||||||||||||||||||||
| blk.store(I32, &new_length, &len_ptr); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -337,6 +402,21 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||
| slow.contains("call double @js_array_pop_f64("), | ||||||||||||||||||||||||||||||||||||||||
| "{what}: the runtime pop must remain the fallback:\n{slow}" | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| // An elements-backed Array subclass resolves its payload through the | ||||||||||||||||||||||||||||||||||||||||
| // meta record (`ObjectMeta.elements`, word 12) and pops from it with | ||||||||||||||||||||||||||||||||||||||||
| // the same length/read/take blocks — no runtime entry for that case. | ||||||||||||||||||||||||||||||||||||||||
| let elements = super::super::class_field_barrier_tests::block_body(ir, "apop.elements.") | ||||||||||||||||||||||||||||||||||||||||
| .expect("the elements probe block exists"); | ||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||
| elements.contains("getelementptr i64, ptr %") && elements.contains(", i64 12"), | ||||||||||||||||||||||||||||||||||||||||
| "{what}: the probe must load ObjectMeta.elements at word 12:\n{elements}" | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| let check = super::super::class_field_barrier_tests::block_body(ir, "apop.elements.check.") | ||||||||||||||||||||||||||||||||||||||||
| .expect("the store validation block exists"); | ||||||||||||||||||||||||||||||||||||||||
| assert!( | ||||||||||||||||||||||||||||||||||||||||
| check.contains(", 1031") && check.contains("icmp eq i8"), | ||||||||||||||||||||||||||||||||||||||||
| "{what}: the store must clear the same integrity mask as a plain array:\n{check}" | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Both `pop` routes — the erased class-field receiver | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Gate the elements probe before loading ObjectHeader metadata.
Line 110 sends every non-plain-array heap value to
apop.elements. This includes non-Object GC allocations. Lines 122-124 then read anObjectHeader::metaslot beforeis_objectcan reject the value.Branch only non-forwarded
GC_TYPE_OBJECTreceivers to the elements probe. Send every other failed array admission directly toapop.slow. This preserves the runtime fallback for dynamically mismatched receivers and prevents an invalid header-layout dereference.🤖 Prompt for AI Agents