Skip to content
Merged
1 change: 1 addition & 0 deletions changelog.d/6811-object-write-fast-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
perf(runtime, codegen): accelerate ordinary existing-field writes with header-first receiver classification and static-key write PICs, and version dense same-shape two-field numeric object loops into a once-validated call-free clone. The #6759 write micro now measures 5 ms in Perry versus 7 ms in Node.js, while descriptors, holes, mixed layouts, proxies, and diagnostic modes retain the full semantic fallback.
11 changes: 11 additions & 0 deletions crates/perry-codegen/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,17 @@ impl LlBlock {
r
}

/// Sequentially-consistent atomic load for globals shared with runtime
/// atomics. The explicit alignment is required by LLVM atomic loads.
pub fn load_atomic_seq_cst(&mut self, ty: LlvmType, ptr: &str, alignment: u32) -> String {
let r = self.reg();
self.emit(format!(
"{} = load atomic {}, ptr {} seq_cst, align {}",
r, ty, ptr, alignment
));
r
}

/// (Issue #52) Load tagged with `!invariant.load !0`. LLVM's GVN +
/// LICM are allowed to hoist these loads out of any enclosing loop —
/// the contract is that the loaded memory does not change between
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ pub(super) fn compile_closure(
integer_locals: native_facts.integer_locals(),
unsigned_i32_locals: native_facts.unsigned_i32_locals(),
shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt,
arena_state_slot: None,
class_keys_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ pub(super) fn compile_module_entry(
integer_locals: main_native_facts.integer_locals(),
unsigned_i32_locals: main_native_facts.unsigned_i32_locals(),
shadow_slot_map: main_shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt: main_shadow_slot_clears_after_stmt,
arena_state_slot: None,
class_keys_slots: HashMap::new(),
Expand Down Expand Up @@ -1347,6 +1348,7 @@ pub(super) fn compile_module_entry(
integer_locals: init_native_facts.integer_locals(),
unsigned_i32_locals: init_native_facts.unsigned_i32_locals(),
shadow_slot_map: init_shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt: init_shadow_slot_clears_after_stmt,
arena_state_slot: None,
class_keys_slots: HashMap::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ pub(super) fn compile_function(
integer_locals: native_facts.integer_locals(),
unsigned_i32_locals: native_facts.unsigned_i32_locals(),
shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt,
arena_state_slot: None,
class_keys_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ pub(super) fn compile_method(
integer_locals: native_facts.integer_locals(),
unsigned_i32_locals: native_facts.unsigned_i32_locals(),
shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt,
arena_state_slot: None,
class_keys_slots: HashMap::new(),
Expand Down Expand Up @@ -1458,6 +1459,7 @@ pub(super) fn compile_static_method(
integer_locals: native_facts.integer_locals(),
unsigned_i32_locals: native_facts.unsigned_i32_locals(),
shadow_slot_map,
persistent_shadow_slots: std::collections::HashSet::new(),
shadow_slot_clears_after_stmt,
arena_state_slot: None,
class_keys_slots: HashMap::new(),
Expand Down
111 changes: 90 additions & 21 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ use super::{
array_kind_fact, buffer_access_materialization_reason, emit_typed_feedback_register_site,
expr_has_numeric_pointer_free_array_layout, int_range_expr, lower_buffer_load, lower_expr,
lower_expr_as_i32, lower_typed_array_load, materialize_js_value, raw_f64_layout_fact,
try_lower_flat_const_index_get, unbox_str_handle, unbox_to_i64, BufferAccessSpec, FnCtx,
PackedF64LoopFact, TypedFeedbackContract, TypedFeedbackKind,
try_lower_flat_const_index_get, typed_feedback_emission_enabled, unbox_str_handle,
unbox_to_i64, BufferAccessSpec, FnCtx, PackedF64LoopFact, TypedFeedbackContract,
TypedFeedbackKind,
};

fn is_width_tracked_typed_array_receiver(ctx: &FnCtx<'_>, object: &Expr) -> bool {
Expand Down Expand Up @@ -345,26 +346,94 @@ fn lower_guarded_array_index_get(
let fallback_label = ctx.block_label(fallback_idx);
let merge_label = ctx.block_label(merge_idx);

let guard_ok = {
let blk = ctx.block();
let guard_fn = if require_numeric_layout {
"js_typed_feedback_numeric_array_index_get_guard"
} else {
"js_typed_feedback_plain_array_index_get_guard"
if !require_numeric_layout && !typed_feedback_emission_enabled() {
// Normal builds do not collect feedback. Inline the plain-array
// structural guard instead of paying an out-of-line call merely to
// rediscover the same header facts before the direct slot load below.
// Prototype-chain invalidators are summarized by one sticky runtime
// byte; per-array descriptors and forwarding remain receiver-local.
let deref_idx = ctx.new_block(&format!("{}.guard.deref", block_prefix));
let deref_label = ctx.block_label(deref_idx);
{
let blk = ctx.block();
let arr_bits = blk.bitcast_double_to_i64(arr_box);
let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64);
let tag = blk.lshr(I64, &arr_bits, "48");
let is_pointer = blk.icmp_eq(I64, &tag, "32765"); // POINTER_TAG
let above_handle_band = blk.icmp_ugt(I64, &arr_handle, "1048575");
let heap_candidate = blk.and(I1, &is_pointer, &above_handle_band);
blk.cond_br(&heap_candidate, &deref_label, &fallback_label);
}

ctx.current_block = deref_idx;
{
let blk = ctx.block();
let arr_bits = blk.bitcast_double_to_i64(arr_box);
let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64);

let gc_type_addr = blk.sub(I64, &arr_handle, "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, "1"); // GC_TYPE_ARRAY

let gc_flags_addr = blk.sub(I64, &arr_handle, "7");
let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr);
let gc_flags = blk.load(I8, &gc_flags_ptr);
let forwarded_bits = blk.and(I8, &gc_flags, "128");
let not_forwarded = blk.icmp_eq(I8, &forwarded_bits, "0");

let reserved_addr = blk.sub(I64, &arr_handle, "6");
let reserved_ptr = blk.inttoptr(I64, &reserved_addr);
let reserved = blk.load(I16, &reserved_ptr);
let descriptor_bits = blk.and(I16, &reserved, "1024");
let no_descriptors = blk.icmp_eq(I16, &descriptor_bits, "0");

let invalidated = blk.load_volatile(I8, "@PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED");
let default_prototype_chain = blk.icmp_eq(I8, &invalidated, "0");

let arr_ptr = blk.inttoptr(I64, &arr_handle);
let length = blk.load(I32, &arr_ptr);
let capacity_ptr = blk.gep(I8, &arr_ptr, &[(I64, "4")]);
let capacity = blk.load(I32, &capacity_ptr);
let index_nonnegative = blk.icmp_slt(I32, idx_i32, "0");
let index_nonnegative = blk.icmp_eq(I1, &index_nonnegative, "false");
let index_in_bounds = blk.icmp_ult(I32, idx_i32, &length);
let length_sane = blk.icmp_ule(I32, &length, "16000000");
let capacity_sane = blk.icmp_ule(I32, &capacity, "16000000");
let length_within_capacity = blk.icmp_ule(I32, &length, &capacity);

let mut guard_ok = blk.and(I1, &is_array, &not_forwarded);
guard_ok = blk.and(I1, &guard_ok, &no_descriptors);
guard_ok = blk.and(I1, &guard_ok, &default_prototype_chain);
guard_ok = blk.and(I1, &guard_ok, &index_nonnegative);
guard_ok = blk.and(I1, &guard_ok, &index_in_bounds);
guard_ok = blk.and(I1, &guard_ok, &length_sane);
guard_ok = blk.and(I1, &guard_ok, &capacity_sane);
guard_ok = blk.and(I1, &guard_ok, &length_within_capacity);
blk.cond_br(&guard_ok, &fast_label, &fallback_label);
}
} else {
let guard_ok = {
let blk = ctx.block();
let guard_fn = if require_numeric_layout {
"js_typed_feedback_numeric_array_index_get_guard"
} else {
"js_typed_feedback_plain_array_index_get_guard"
};
let guard_i32 = blk.call(
I32,
guard_fn,
&[
(I64, &feedback_site_id),
(DOUBLE, arr_box),
(I32, idx_i32),
(I32, "1"),
],
);
blk.icmp_ne(I32, &guard_i32, "0")
};
let guard_i32 = blk.call(
I32,
guard_fn,
&[
(I64, &feedback_site_id),
(DOUBLE, arr_box),
(I32, idx_i32),
(I32, "1"),
],
);
blk.icmp_ne(I32, &guard_i32, "0")
};
ctx.block().cond_br(&guard_ok, &fast_label, &fallback_label);
ctx.block().cond_br(&guard_ok, &fast_label, &fallback_label);
}

ctx.current_block = fallback_idx;
// Materialize the f64 index only here (cold path) so the int→fp conversion
Expand Down
11 changes: 9 additions & 2 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ pub(crate) use range_facts::{
};
pub(crate) use strings::emit_string_literal_global;
pub(crate) use typed_feedback::{
emit_typed_feedback_register_site, native_region_slug, TypedFeedbackContract, TypedFeedbackKind,
emit_typed_feedback_register_site, native_region_slug, typed_feedback_emission_enabled,
TypedFeedbackContract, TypedFeedbackKind,
};
pub(crate) use url_helpers::lower_url_string_getter;
pub(crate) use v8_interop::{
Expand All @@ -128,7 +129,7 @@ mod shadow_slot;
pub(crate) use dispatch::{lower_expr, lower_math_operand};
pub(crate) use shadow_slot::{
emit_shadow_slot_bind_for_local, emit_shadow_slot_clear, emit_shadow_slot_update_for_expr,
expr_is_known_non_pointer_shadow_value,
enable_persistent_shadow_slot_for_array_alias, expr_is_known_non_pointer_shadow_value,
};

/// One in-flight inline-constructor return target. See
Expand Down Expand Up @@ -598,6 +599,12 @@ pub(crate) struct FnCtx<'a> {
/// the frame reflects the live pointer state at the following
/// safepoint. Today — just tracked, not consumed.
pub shadow_slot_map: std::collections::HashMap<u32, u32>,
/// Shadow slots bound once in the function-entry setup and deliberately
/// kept active until return. This is used for immutable loop aliases read
/// from an already-rooted array: the local alloca is stable, and retaining
/// its current value for the function lifetime avoids per-iteration TLS
/// bind/clear traffic without weakening GC reachability.
pub persistent_shadow_slots: std::collections::HashSet<u32>,
/// Top-level statement index → shadow-frame slot indices that can be
/// cleared after lowering that statement. Built once per user function
/// from HIR local-reference last-use information.
Expand Down
Loading
Loading