Skip to content

Commit 1598a8f

Browse files
author
Ralph Küpper
committed
perf(map): repair ordered-delete indexes in place
1 parent db821b5 commit 1598a8f

3 files changed

Lines changed: 174 additions & 57 deletions

File tree

crates/perry-runtime/src/gc/barrier/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1932,7 +1932,7 @@ pub(crate) fn runtime_store_external_jsvalue_slot_with_layout(
19321932
runtime_write_barrier_external_slot(parent_user, slot_addr, value_bits);
19331933
}
19341934

1935-
pub(crate) fn runtime_dirty_external_slot_span(
1935+
pub(crate) fn runtime_write_barrier_external_slot_span(
19361936
parent_addr: usize,
19371937
first_slot_addr: usize,
19381938
slot_count: usize,

crates/perry-runtime/src/map.rs

Lines changed: 171 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,7 +1365,7 @@ unsafe fn map_set_string_key_value(
13651365
let size = (*map).size;
13661366
let entries = entries_ptr_mut(map);
13671367
if grew && size > 0 {
1368-
crate::gc::runtime_dirty_external_slot_span(
1368+
crate::gc::runtime_write_barrier_external_slot_span(
13691369
map as usize,
13701370
entries as usize,
13711371
size as usize * 2,
@@ -1474,7 +1474,7 @@ fn map_set_resolved(map: *mut MapHeader, key: f64, value: f64) {
14741474
let size = (*map).size;
14751475
let entries = entries_ptr_mut(map);
14761476
if grew && size > 0 {
1477-
crate::gc::runtime_dirty_external_slot_span(
1477+
crate::gc::runtime_write_barrier_external_slot_span(
14781478
map as usize,
14791479
entries as usize,
14801480
size as usize * 2,
@@ -1843,79 +1843,92 @@ unsafe fn delete_entry_at_index(map: *mut MapHeader, idx: i32) -> i32 {
18431843
return 0;
18441844
}
18451845
let entries = entries_ptr_mut(map);
1846+
let deleted_key = ptr::read(entries.add(idx * 2));
18461847

18471848
// #2831: preserve insertion order. JS Map iteration must keep the
18481849
// relative order of surviving entries after a delete (and a
18491850
// delete-then-re-add appends at the end). The previous swap-and-pop
1850-
// moved the last entry into the hole, reordering iteration. Shift
1851-
// every entry after `idx` down by one slot instead.
1852-
for i in idx..(size as usize - 1) {
1853-
let next_key = ptr::read(entries.add((i + 1) * 2));
1854-
let next_value = ptr::read(entries.add((i + 1) * 2 + 1));
1855-
// GC_STORE_AUDIT(EXTERNAL_BARRIERED): map compaction slots use the shared external-slot helper.
1856-
crate::gc::runtime_store_external_jsvalue_slot(
1857-
map as usize,
1858-
entries.add(i * 2) as usize,
1859-
next_key.to_bits(),
1851+
// moved the last entry into the hole, reordering iteration. Compact the
1852+
// already-owned key/value pairs with one overlap-safe move. This does not
1853+
// create a new parent -> child edge: every copied value was already in
1854+
// this Map. The span mark preserves the old -> young remembered-set
1855+
// contract for the slots' new addresses without paying two full runtime
1856+
// stores per entry.
1857+
let moved_entries = size as usize - idx - 1;
1858+
if moved_entries > 0 {
1859+
// GC_STORE_AUDIT(EXTERNAL_BARRIERED): ordered compaction is followed by a dirty-span barrier for every moved slot.
1860+
ptr::copy(
1861+
entries.add((idx + 1) * 2),
1862+
entries.add(idx * 2),
1863+
moved_entries * 2,
18601864
);
1861-
crate::gc::runtime_store_external_jsvalue_slot(
1865+
crate::gc::runtime_write_barrier_external_slot_span(
18621866
map as usize,
1863-
entries.add(i * 2 + 1) as usize,
1864-
next_value.to_bits(),
1867+
entries.add(idx * 2) as usize,
1868+
moved_entries * 2,
18651869
);
18661870
}
18671871

18681872
(*map).size = size - 1;
18691873

1870-
// The shift changes the entry index of every surviving key at or
1871-
// after `idx`, so the O(1) lookup side-tables can't be patched in
1872-
// place cheaply. Rebuild them from the compacted buffer.
1873-
rebuild_map_index(map);
1874+
// The old implementation rebuilt all three indexes from the entries
1875+
// buffer after every ordered delete. Repair their existing u32 offsets
1876+
// in place instead: removing one key and decrementing later offsets is a
1877+
// cache-linear pass over index values and does not re-hash surviving keys.
1878+
repair_map_indices_after_ordered_delete(map, deleted_key, idx as u32);
18741879
1
18751880
}
18761881

1877-
/// Rebuild the numeric + string lookup side-tables for `map` from its
1878-
/// current compacted entries buffer. Used after an order-preserving
1879-
/// `delete` shifts entry indexes (#2831).
1880-
unsafe fn rebuild_map_index(map: *mut MapHeader) {
1881-
if map.is_null() {
1882-
return;
1883-
}
1884-
let size = (*map).size as usize;
1885-
let capacity = (*map).capacity as usize;
1886-
if size > capacity || size > 16_000_000 || (*map).entries.is_null() {
1887-
return;
1888-
}
1889-
let entries = entries_ptr(map);
1890-
MAP_INDEX.with(|idx| {
1891-
let mut idx = idx.borrow_mut();
1892-
let slot = idx
1893-
.entry(map as usize)
1894-
.or_insert_with(crate::fast_hash::new_ptr_hash_map);
1895-
slot.clear();
1896-
for i in 0..size {
1897-
let key_bits = ptr::read(entries.add(i * 2)).to_bits();
1898-
if is_safe_numeric_key(key_bits) {
1899-
slot.insert(NumericKey(key_bits), i as u32);
1882+
unsafe fn repair_map_indices_after_ordered_delete(
1883+
map: *mut MapHeader,
1884+
deleted_key: f64,
1885+
deleted_idx: u32,
1886+
) {
1887+
let map_addr = map as usize;
1888+
let deleted_bits = deleted_key.to_bits();
1889+
1890+
MAP_INDEX.with(|indexes| {
1891+
let mut indexes = indexes.borrow_mut();
1892+
if let Some(index) = indexes.get_mut(&map_addr) {
1893+
if is_safe_numeric_key(deleted_bits) {
1894+
index.remove(&NumericKey(deleted_bits));
1895+
}
1896+
for entry_idx in index.values_mut() {
1897+
if *entry_idx > deleted_idx {
1898+
*entry_idx -= 1;
1899+
}
19001900
}
19011901
}
19021902
});
1903-
MAP_STRING_INDEX.with(|idx| {
1904-
let mut idx = idx.borrow_mut();
1905-
let slot = idx
1906-
.entry(map as usize)
1907-
.or_insert_with(std::collections::HashMap::new);
1908-
slot.clear();
1909-
for i in 0..size {
1910-
let key_bits = ptr::read(entries.add(i * 2)).to_bits();
1911-
if is_string_like(key_bits) {
1912-
if let Some(h) = string_content_hash(key_bits) {
1913-
slot.entry(h).or_insert_with(Vec::new).push(i as u32);
1903+
1904+
MAP_STRING_INDEX.with(|indexes| {
1905+
let mut indexes = indexes.borrow_mut();
1906+
if let Some(index) = indexes.get_mut(&map_addr) {
1907+
for bucket in index.values_mut() {
1908+
bucket.retain(|entry_idx| *entry_idx != deleted_idx);
1909+
for entry_idx in bucket {
1910+
if *entry_idx > deleted_idx {
1911+
*entry_idx -= 1;
1912+
}
1913+
}
1914+
}
1915+
index.retain(|_, bucket| !bucket.is_empty());
1916+
}
1917+
});
1918+
1919+
MAP_PTR_INDEX.with(|indexes| {
1920+
let mut indexes = indexes.borrow_mut();
1921+
if let Some(index) = indexes.get_mut(&map_addr) {
1922+
if is_ptr_index_key(deleted_bits) {
1923+
index.remove(&MapPtrKey(deleted_key));
1924+
}
1925+
for entry_idx in index.values_mut() {
1926+
if *entry_idx > deleted_idx {
1927+
*entry_idx -= 1;
19141928
}
19151929
}
19161930
}
19171931
});
1918-
rebuild_map_ptr_index(map);
19191932
}
19201933

19211934
/// Rebuild ONLY the pointer-key index for `map` from its current entries
@@ -2687,4 +2700,108 @@ mod tests {
26872700
assert_eq!(js_map_delete_number_key(map, boxed_string_key), 1);
26882701
assert_eq!(js_map_has(map, boxed_string_key), 0);
26892702
}
2703+
2704+
#[test]
2705+
fn ordered_delete_repairs_mixed_side_indexes_and_preserves_order() {
2706+
let map = js_map_alloc(32);
2707+
let string_keys = (0..12)
2708+
.map(|i| {
2709+
let bytes = format!("key-{i:02}").into_bytes();
2710+
js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32)
2711+
})
2712+
.collect::<Vec<_>>();
2713+
2714+
for (i, string_key) in string_keys.iter().copied().enumerate() {
2715+
js_map_set(map, i as f64, (i * 10) as f64);
2716+
js_map_set_string_number(map, string_key, (i * 10 + 1) as f64);
2717+
}
2718+
// Keep the backing allocations alive while using their tagged
2719+
// addresses as identity keys. They deliberately are not GC objects:
2720+
// this exercises the pointer-key index without introducing an
2721+
// allocation/collection point into the ordered-delete fixture.
2722+
let pointer_owners = (0..4).map(Box::new).collect::<Vec<_>>();
2723+
let pointer_keys = pointer_owners
2724+
.iter()
2725+
.map(|owner| {
2726+
f64::from_bits(
2727+
crate::value::POINTER_TAG
2728+
| ((owner.as_ref() as *const i32 as u64) & crate::value::POINTER_MASK),
2729+
)
2730+
})
2731+
.collect::<Vec<_>>();
2732+
for (i, key) in pointer_keys.iter().copied().enumerate() {
2733+
js_map_set(map, key, (1_000 + i) as f64);
2734+
}
2735+
assert_eq!(js_map_size(map), 28);
2736+
2737+
assert_eq!(js_map_delete_number_key(map, 2.0), 1);
2738+
assert_eq!(js_map_delete_string_key(map, string_keys[4]), 1);
2739+
assert_eq!(js_map_delete(map, pointer_keys[1]), 1);
2740+
assert_eq!(js_map_size(map), 25);
2741+
assert_eq!(js_map_has_number_key(map, 2.0), 0);
2742+
assert_eq!(js_map_has_string_key(map, string_keys[4]), 0);
2743+
assert_eq!(js_map_has(map, pointer_keys[1]), 0);
2744+
2745+
for (i, string_key) in string_keys.iter().copied().enumerate() {
2746+
if i != 2 {
2747+
assert_eq!(js_map_get_number_key(map, i as f64), (i * 10) as f64);
2748+
assert!(test_map_numeric_index_contains(map, i as f64));
2749+
}
2750+
if i != 4 {
2751+
assert_eq!(js_map_get_string_key(map, string_key), (i * 10 + 1) as f64);
2752+
assert!(test_map_string_index_contains(
2753+
map,
2754+
boxed_heap_string_key(string_key)
2755+
));
2756+
}
2757+
}
2758+
for (i, key) in pointer_keys.iter().copied().enumerate() {
2759+
if i != 1 {
2760+
assert_eq!(js_map_get(map, key), (1_000 + i) as f64);
2761+
assert!(test_map_ptr_index_contains(map, key));
2762+
}
2763+
}
2764+
2765+
let mut expected_keys = (0..12)
2766+
.flat_map(|i| {
2767+
let mut keys = Vec::new();
2768+
if i != 2 {
2769+
keys.push((i as f64).to_bits());
2770+
}
2771+
if i != 4 {
2772+
keys.push(boxed_heap_string_key(string_keys[i]).to_bits());
2773+
}
2774+
keys
2775+
})
2776+
.collect::<Vec<_>>();
2777+
expected_keys.extend(
2778+
pointer_keys
2779+
.iter()
2780+
.enumerate()
2781+
.filter(|(i, _)| *i != 1)
2782+
.map(|(_, key)| key.to_bits()),
2783+
);
2784+
let actual_keys = (0..js_map_size(map))
2785+
.map(|i| js_map_entry_key_at(map, i).to_bits())
2786+
.collect::<Vec<_>>();
2787+
assert_eq!(
2788+
actual_keys, expected_keys,
2789+
"delete must preserve survivor order"
2790+
);
2791+
2792+
js_map_set_number_key(map, 2.0, 222.0);
2793+
js_map_set_string_number(map, string_keys[4], 444.0);
2794+
js_map_set(map, pointer_keys[1], 1_111.0);
2795+
assert_eq!(js_map_size(map), 28);
2796+
assert_eq!(js_map_entry_key_at(map, 25).to_bits(), 2.0f64.to_bits());
2797+
assert_eq!(
2798+
js_map_entry_key_at(map, 26).to_bits(),
2799+
boxed_heap_string_key(string_keys[4]).to_bits(),
2800+
"delete-then-re-add must append at the end"
2801+
);
2802+
assert_eq!(
2803+
js_map_entry_key_at(map, 27).to_bits(),
2804+
pointer_keys[1].to_bits()
2805+
);
2806+
}
26902807
}

crates/perry-runtime/src/set.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ fn set_add_resolved(set: *mut SetHeader, value: f64) {
988988
let size = (*set).size;
989989
let elements = elements_ptr_mut(set);
990990
if grew && size > 0 {
991-
crate::gc::runtime_dirty_external_slot_span(
991+
crate::gc::runtime_write_barrier_external_slot_span(
992992
set as usize,
993993
elements as usize,
994994
size as usize,
@@ -1045,7 +1045,7 @@ fn set_add_string_resolved(set: *mut SetHeader, value: *const StringHeader) {
10451045
let size = (*set).size;
10461046
let elements = elements_ptr_mut(set);
10471047
if grew && size > 0 {
1048-
crate::gc::runtime_dirty_external_slot_span(
1048+
crate::gc::runtime_write_barrier_external_slot_span(
10491049
set as usize,
10501050
elements as usize,
10511051
size as usize,

0 commit comments

Comments
 (0)