-
-
Notifications
You must be signed in to change notification settings - Fork 158
perf(gc): skip tiny per-object layout masks #8539
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
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,17 @@ | ||
| ### Performance | ||
|
|
||
| - **Cut `interp` retired instructions by 4.61% by keeping tiny mixed GC | ||
| payloads out of the per-object layout tables.** A 30-run sample profile put | ||
| 61.1% of self samples in `evalNode`; the result did not support the suspected | ||
| dynamic-numeric-envelope explanation. Instead, its pointer-bearing | ||
| environment arrays were paying to create and later clean up per-object | ||
| layout masks even though those masks could skip only a few exact tag checks. | ||
|
|
||
| Raise the minimum mask-bearing payload from two slots to four, so one-, two-, | ||
| and three-slot payloads use the collector's exact tag scan. A cutoff sweep | ||
| found that four captured the full `interp` instruction win; moving the cutoff | ||
| to eight retired no fewer instructions, while it would make larger objects | ||
| perform extra tag checks and broaden the GC policy change. Across the | ||
| 19-benchmark corpus, `interp` moved 9,013,545,066 → 8,598,251,738 retired | ||
| instructions (−4.61%), no other row moved by more than 1%, and peak RSS did | ||
| not regress. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -158,13 +158,15 @@ fn test_layout_scan_trace_json_counts_pointer_slot_bytes() { | |
| ) | ||
| .expect("test requested GC trace capture"); | ||
| let child = crate::string::js_string_from_bytes(b"byte-child".as_ptr(), 10) as *mut u8; | ||
| let arr = crate::array::js_array_alloc_with_length(2); | ||
| let arr = crate::array::js_array_alloc_with_length(4); | ||
| crate::array::js_array_set_f64(arr, 0, 1.0); | ||
| crate::array::js_array_set_f64( | ||
| arr, | ||
| 1, | ||
| f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), | ||
| ); | ||
| crate::array::js_array_set_f64(arr, 2, 2.0); | ||
| crate::array::js_array_set_f64(arr, 3, 3.0); | ||
|
Comment on lines
+161
to
+169
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 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the array allocation implementation and available test-root helpers.
fd -t f -e rs . crates/perry-runtime/src | while IFS= read -r file; do
rg -n -C 3 'fn js_array_alloc_with_length|js_array_alloc_with_length\(' "$file" || true
done
rg -n -C 3 'js_shadow_slot_set|RootGuard|root.*slot|shadow.*slot' \
crates/perry-runtime/src/gc/tests crates/perry-runtime/srcRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== array allocation definitions and callers =='
rg -n -C 12 'pub\s+unsafe\s+fn\s+js_array_alloc_with_length|fn\s+js_array_alloc_with_length|js_array_alloc_with_length' \
crates/perry-runtime/src/array.rs crates/perry-runtime/src/gc/tests/layout_trace.rs \
crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs \
crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs
printf '%s\n' '== focused test contexts =='
sed -n '130,185p;220,315p' crates/perry-runtime/src/gc/tests/layout_trace.rs
sed -n '75,125p;165,230p' crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs
sed -n '575,630p' crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs
printf '%s\n' '== root helpers and test setup in the focused files =='
rg -n -C 5 'js_shadow_frame_push|js_shadow_frame_pop|js_shadow_slot_set|GcTestIsolationGuard|CopyingNurseryTestGuard|full_gc|gc_collect|clear_mark' \
crates/perry-runtime/src/gc/tests/layout_trace.rs \
crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs \
crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rsRepository: PerryTS/perry Length of output: 40887 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== candidate array files =='
fd -t f -i 'array.*\.rs$|.*array.*\.rs$' crates/perry-runtime | head -80
printf '%s\n' '== allocation definitions =='
rg -n -C 15 'fn\s+js_array_alloc_with_length|pub\s+.*js_array_alloc_with_length|js_array_alloc_with_length\s*=' crates/perry-runtime
printf '%s\n' '== focused-file headers and test-root imports =='
sed -n '1,45p' crates/perry-runtime/src/gc/tests/layout_trace.rs
sed -n '1,35p' crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs
sed -n '1,35p' crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs
printf '%s\n' '== exact root helper definitions =='
rg -n -C 12 'fn\s+(js_shadow_frame_push|js_shadow_frame_pop|js_shadow_slot_set|js_shadow_slot_get)|pub\s+.*(js_shadow_frame_push|js_shadow_frame_pop|js_shadow_slot_set)' crates/perry-runtimeRepository: PerryTS/perry Length of output: 23456 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== arena_alloc_gc implementation =='
rg -n -C 25 'fn\s+arena_alloc_gc|pub\s+.*arena_alloc_gc|arena_alloc_gc\(' \
crates/perry-runtime/src/arena crates/perry-runtime/src/gc crates/perry-runtime/src/array/alloc.rs | head -260
printf '%s\n' '== trigger guards and their effects =='
rg -n -C 18 'struct\s+GcTriggerThresholdTestGuard|impl\s+GcTriggerThresholdTestGuard|suppress_automatic_triggers|automatic.*trigger|GC_TRIGGER' \
crates/perry-runtime/src/gc crates/perry-runtime/src/arena crates/perry-runtime/src/gc/tests/layout_trace
printf '%s\n' '== test root frame setup in shared support =='
rg -n -C 15 'js_shadow_frame_push|js_shadow_frame_pop|js_shadow_slot_set|CopyingNurseryTestGuard|GcTestIsolationGuard' \
crates/perry-runtime/src/gc/tests/support.rs crates/perry-runtime/src/gc/tests crates/perry-runtime/src/gc/mod.rs 2>/dev/null | head -320Repository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== arena_alloc_gc definition locations =='
rg -l 'fn\s+arena_alloc_gc|pub\s+.*arena_alloc_gc' crates/perry-runtime/src | head -20
printf '%s\n' '== allocator definition =='
for file in $(rg -l 'fn\s+arena_alloc_gc|pub\s+.*arena_alloc_gc' crates/perry-runtime/src | head -5); do
rg -n -C 35 'fn\s+arena_alloc_gc|pub\s+.*arena_alloc_gc' "$file"
done
printf '%s\n' '== focused test support definitions =='
rg -l 'struct\s+GcTriggerThresholdTestGuard|impl\s+GcTriggerThresholdTestGuard|struct\s+CopyingNurseryTestGuard' crates/perry-runtime/src/gc/tests | head -20
for file in $(rg -l 'struct\s+GcTriggerThresholdTestGuard|impl\s+GcTriggerThresholdTestGuard|struct\s+CopyingNurseryTestGuard' crates/perry-runtime/src/gc/tests | head -5); do
rg -n -C 35 'struct\s+GcTriggerThresholdTestGuard|impl\s+GcTriggerThresholdTestGuard|struct\s+CopyingNurseryTestGuard' "$file"
done
printf '%s\n' '== target tests: guards and allocations only =='
rg -n -B 8 -A 3 'fn test_|let (child|child0|child1|src|arr|mixed|cloned|concatenated) =|js_array_alloc_with_length' \
crates/perry-runtime/src/gc/tests/layout_trace.rs \
crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs \
crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rsRepository: PerryTS/perry Length of output: 50369 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== arena_alloc_gc collection path =='
sed -n '398,525p' crates/perry-runtime/src/arena/allocators.rs
printf '%s\n' '== rootable handle APIs =='
rg -n -C 15 'struct\s+RuntimeHandleScope|impl\s+RuntimeHandleScope|root_raw|root_nanbox|fn\s+get_raw|fn\s+get_nanbox' \
crates/perry-runtime/src/gc crates/perry-runtime/src | head -300
printf '%s\n' '== target test bodies with complete context =='
sed -n '145,190p;235,275p;275,340p' crates/perry-runtime/src/gc/tests/layout_trace.rs
sed -n '90,145p;184,235p' crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs
sed -n '590,635p' crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs
printf '%s\n' '== all shadow-frame operations in layout-trace tests =='
rg -n 'js_shadow_frame_push|js_shadow_frame_pop|js_shadow_slot_set|js_shadow_slot_get|RuntimeHandleScope' \
crates/perry-runtime/src/gc/tests/layout_trace.rs \
crates/perry-runtime/src/gc/tests/layout_trace/*.rsRepository: PerryTS/perry Length of output: 47604 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== arena_alloc trigger path =='
rg -n -C 35 'pub\s+fn\s+arena_alloc\(|fn\s+arena_alloc\(' crates/perry-runtime/src/arena/allocators.rs
rg -n -C 12 'gc_check_trigger\(' crates/perry-runtime/src/arena/allocators.rs crates/perry-runtime/src/gc | head -160
printf '%s\n' '== RuntimeHandleScope implementation =='
rg -l 'pub\s+struct\s+RuntimeHandleScope|struct\s+RuntimeHandleScope' crates/perry-runtime/src | head -20
for file in $(rg -l 'pub\s+struct\s+RuntimeHandleScope|struct\s+RuntimeHandleScope' crates/perry-runtime/src | head -5); do
rg -n -C 35 'pub\s+struct\s+RuntimeHandleScope|struct\s+RuntimeHandleScope|root_raw_mut_ptr|root_nanbox_f64|root_nanbox_u64' "$file"
done
printf '%s\n' '== test guard imports and available mutable-root scanner registration =='
rg -n -C 8 'register_runtime_handle_root_scanner|scan_runtime_handle_roots_mut|gc_register_mutable_root_scanner_with_source' \
crates/perry-runtime/src/gc/tests/layout_trace crates/perry-runtime/src/gc/tests/support.rsRepository: PerryTS/perry Length of output: 28623 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== arena_cell_alloc and trigger call =='
rg -n -C 45 'fn\s+arena_cell_alloc|pub\s+.*arena_cell_alloc' crates/perry-runtime/src/arena
rg -n -C 10 'gc_check_trigger\(' crates/perry-runtime/src/arena | head -120
printf '%s\n' '== read-only verifier for the consolidated sites =='
python3 - <<'PY'
from pathlib import Path
import re
sites = {
"crates/perry-runtime/src/gc/tests/layout_trace.rs": [161, 245, 290],
"crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs": [102, 192],
"crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs": [605],
}
alloc = Path("crates/perry-runtime/src/array/alloc.rs").read_text()
arena = Path("crates/perry-runtime/src/arena/allocators.rs").read_text()
assert "arena_alloc_gc(" in alloc
assert "arena_alloc(" in arena
assert "gc_check_trigger" in Path("crates/perry-runtime/src/arena/arena.rs").read_text() if Path("crates/perry-runtime/src/arena/arena.rs").exists() else True
for name, lines in sites.items():
text = Path(name).read_text().splitlines()
print(name)
for line in lines:
start = max(0, line - 4)
end = min(len(text), line + 15)
block = "\n".join(text[start:end])
has_alloc = "js_array_alloc_with_length" in block
has_root_before = any(
token in "\n".join(text[max(0, start-40):start])
for token in ("RuntimeHandleScope", "js_shadow_slot_set", "root_raw", "root_nanbox")
)
print(f" line {line}: allocation={has_alloc}, preceding_root_api={has_root_before}")
print("allocator_chain: js_array_alloc_with_length -> arena_alloc_gc -> arena_alloc")
PYRepository: PerryTS/perry Length of output: 19347 Root and refresh every live heap pointer across allocation points.
This also covers 📍 Affects 3 files
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| let valid_ptrs = build_valid_pointer_set(); | ||
| assert!(try_mark_value( | ||
|
|
@@ -240,16 +242,17 @@ fn test_layout_mask_small_mixed_array_scans_exact_pointer_slot() { | |
|
|
||
| let child = crate::string::js_string_from_bytes(b"array-child".as_ptr(), 11) as *mut u8; | ||
| let child_header = unsafe { header_from_user_ptr(child) }; | ||
| let arr = crate::array::js_array_alloc_with_length(3); | ||
| let arr = crate::array::js_array_alloc_with_length(4); | ||
| crate::array::js_array_set_f64(arr, 0, 1.0); | ||
| crate::array::js_array_set_f64( | ||
| arr, | ||
| 1, | ||
| f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), | ||
| ); | ||
| crate::array::js_array_set_f64(arr, 2, 3.0); | ||
| crate::array::js_array_set_f64(arr, 3, 4.0); | ||
|
|
||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 3), Some(1)); | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(1)); | ||
|
|
||
| let valid_ptrs = build_valid_pointer_set(); | ||
| let mut worklist = Vec::new(); | ||
|
|
@@ -261,7 +264,7 @@ fn test_layout_mask_small_mixed_array_scans_exact_pointer_slot() { | |
| assert_eq!(test_trace_slot_reads(), 1); | ||
|
|
||
| crate::array::js_array_set_f64(arr, 1, 2.0); | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 3), Some(0)); | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(0)); | ||
|
|
||
| clear_marks(); | ||
| clear_mark_seeds(); | ||
|
|
@@ -284,14 +287,14 @@ fn test_pointer_store_restores_side_mask_from_stale_pointer_free() { | |
| let child0_h = unsafe { header_from_user_ptr(child0) }; | ||
| let child1_h = unsafe { header_from_user_ptr(child1) }; | ||
|
|
||
| let arr = crate::array::js_array_alloc_with_length(2); | ||
| let arr = crate::array::js_array_alloc_with_length(4); | ||
| // Store a pointer at index 0 -> SIDE_MASK + mask{0}. | ||
| crate::array::js_array_set_f64( | ||
| arr, | ||
| 0, | ||
| f64::from_bits(STRING_TAG | (child0 as u64 & POINTER_MASK)), | ||
| ); | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 2), Some(1)); | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(1)); | ||
|
|
||
| // Reproduce the stale-state hazard: force POINTER_FREE while the mask{0} | ||
| // entry is still present (`set_layout_state` only touches the state bits). | ||
|
|
@@ -316,7 +319,7 @@ fn test_pointer_store_restores_side_mask_from_stale_pointer_free() { | |
| "recording a pointer into an existing mask must restore SIDE_MASK" | ||
| ); | ||
| } | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 2), Some(2)); | ||
| assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(2)); | ||
|
|
||
| let valid_ptrs = build_valid_pointer_set(); | ||
| let mut worklist = Vec::new(); | ||
|
|
||
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Hyphenate
self-samples.Use
self-samplesas the compound modifier.🧰 Tools
🪛 LanguageTool
[grammar] ~5-~5: Use a hyphen to join words.
Context: ...0-run sample profile put 61.1% of self samples in
evalNode; the result did no...(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
Source: Linters/SAST tools