Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions changelog.d/9025-set-tombstone-delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Ordered `Set` deletes are O(1) instead of O(N) — the Set twin of #9020.

#8993 removed `Set.delete`'s per-delete index re-hash, but every delete still
shifted the surviving elements down and span-barriered the moved slots, so
emptying a Set stayed O(N²): per-delete cost grew 0.80 µs at N=1k to 5.36 µs at
N=8k, against node's flat ~0.027 µs.

A delete now tombstones the slot in place with the reserved hole marker, the
live count drops while the array extent (`used`, a new `SetHeader` field) stays
put, and raw indices are therefore stable — the lookup index only forgets the
deleted value, and nothing is repaired. Compaction runs when holes outnumber
live elements, before growing, or when a raw-indexed reader observes them.

The marker can never be a stored value: `normalize_zero` canonicalizes it to
`undefined` on every insert path, and compaction only moves values that were
already normalized.

Insertion order is unchanged — iteration walks raw indices and skips holes, and
delete-then-re-add appends at the end. The two raw-slot readers that could have
been defeated by a hole compact first: `js_set_to_array`'s bulk memcpy and the
subset/disjoint element walkers, so a hole can neither leak into an array nor
break a subset check.

The GC contract bounds the element range by `used`, with `size ≤ used ≤ capacity`
as the guard; holes are non-pointer markers the tag-filtered scan skips.
19 changes: 13 additions & 6 deletions crates/perry-runtime/src/collection_iter_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,21 +344,28 @@ unsafe fn dispatch_set_iterator_method_emit(
}
let cursor = f64::from_bits(js_object_get_field(iter_obj(), 1).bits()) as u32;
let last_val = js_object_get_field(iter_obj(), 4);
let size = crate::set::js_set_size(set());
let used = crate::set::set_used_entries(set());
let in_place = cursor > 0 && {
let prev = crate::set::js_set_value_at(set(), cursor - 1);
let prev = crate::set::set_value_raw(set(), cursor - 1);
crate::value::js_jsvalue_same_value_zero(prev, f64::from_bits(last_val.bits())) != 0
};
let idx = next_read_index(cursor, in_place, || {
let mut idx = next_read_index(cursor, in_place, || {
crate::set::find_value_index(set(), f64::from_bits(last_val.bits()))
});
if idx >= size {
js_object_set_field(iter_obj(), 1, JSValue::number(size as f64));
// Tombstoned deletes leave holes in the raw order; step over them.
while idx < used
&& crate::set::set_value_raw(set(), idx).to_bits()
== crate::set::SET_HOLE_VALUE_BITS
{
idx += 1;
}
if idx >= used {
js_object_set_field(iter_obj(), 1, JSValue::number(used as f64));
js_object_set_field(iter_obj(), 0, JSValue::undefined());
return emit_iter_result(&scope, &iter_h, emit_cached, JSValue::undefined(), true);
}

let elem = crate::set::js_set_value_at(set(), idx);
let elem = crate::set::set_value_raw(set(), idx);
js_object_set_field(iter_obj(), 1, JSValue::number((idx + 1) as f64));
js_object_set_field(iter_obj(), 4, JSValue::from_bits(elem.to_bits()));

Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/gc/tests/barrier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,7 @@ fn test_old_young_edge_verifier_accepts_set_external_slot() {
let set_header = unsafe { header_from_user_ptr(set as *const u8) };
unsafe {
(*set).size = 1;
(*set).used = 1;
(*set_header).gc_flags |= GC_FLAG_MARKED;
}
runtime_store_external_jsvalue_slot(set as usize, elements as usize, ptr_bits(young));
Expand Down Expand Up @@ -1120,6 +1121,7 @@ fn test_dirty_page_set_external_slot_marks_child() {
let (set, elements, layout) = unsafe { alloc_old_test_set(1) };
unsafe {
(*set).size = 1;
(*set).used = 1;
}
runtime_store_external_jsvalue_slot(set as usize, elements as usize, ptr_bits(young));

Expand Down Expand Up @@ -1155,6 +1157,7 @@ fn test_rewrite_remembered_dirty_range_updates_set_external_entry_span() {
let (set, elements, layout) = unsafe { alloc_old_test_set(2048) };
unsafe {
(*set).size = 2048;
(*set).used = 2048;
}
let (dirty_idx, clean_idx) = unsafe { field_indices_on_distinct_pages(elements, 2048) };
let dirty_slot = unsafe { elements.add(dirty_idx) };
Expand Down Expand Up @@ -1470,6 +1473,7 @@ fn test_incremental_barrier_marks_external_map_and_set_slots() {
(*map).size = 1;
(*map).used = 1;
(*set).size = 1;
(*set).used = 1;
}
mark_user_ptr(map as usize);
mark_user_ptr(set as usize);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/tests/helper_stores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn map_and_set_external_helper_stores_preserve_young_children() {
let (set, set_elements, set_layout) = unsafe { alloc_old_test_set(1) };
unsafe {
(*set).size = 1;
(*set).used = 1;
crate::gc::runtime_store_external_jsvalue_slot(
set as usize,
set_elements as usize,
Expand Down
Loading
Loading