Skip to content

Commit 2bf1971

Browse files
committed
Count content IDs with direct indexing
AI-assisted change (OpenAI Codex).
1 parent e08bf74 commit 2bf1971

3 files changed

Lines changed: 51 additions & 25 deletions

File tree

PERF_RESEARCH_LOG.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1396,6 +1396,28 @@ side-by-side, inline, and JSON modes on all 107 available sample pairs.
13961396
`cargo test` passes (164 passed, one ignored), and the changed file passes
13971397
`rustfmt --check`.
13981398

1399+
### exp46: count dense content IDs in a vector — KEPT
1400+
1401+
The two uniqueness passes counted `ContentId`s in hash maps. Like syntax IDs,
1402+
content IDs are dense positive integers; after both trees receive IDs, the
1403+
number of possible IDs is already known from the content-key table. The counts
1404+
now use direct vector indexing, and the allocation is cleared and reused for
1405+
the RHS. This removes hashing from both the counting and uniqueness-setting
1406+
walks; exp38 had shown that eliminating a walk while retaining those hashes was
1407+
flat.
1408+
1409+
Ten-run `perf stat` means were:
1410+
1411+
| pair | hashed counts | dense counts | change |
1412+
| --- | ---: | ---: | ---: |
1413+
| `slow` | 514,819,594 | 514,223,778 | **-0.116%** |
1414+
| `typing` | 2,754,599,330 | 2,743,556,655 | **-0.401%** |
1415+
1416+
Peak RSS was flat in single-run checks. Output is byte-identical in
1417+
side-by-side, inline, and JSON modes on all 107 available sample pairs.
1418+
`cargo test` passes (164 passed, one ignored), and the changed file passes
1419+
`rustfmt --check`.
1420+
13991421
## Where this leaves things
14001422

14011423
| workload | earlier reference | now | change |
@@ -1404,8 +1426,8 @@ side-by-side, inline, and JSON modes on all 107 available sample pairs.
14041426
| trivial Rust diff, master through query work | 444,668,795 | ~206,000,000 | **-54%** |
14051427
| `huge_cpp`, before exp17 through exp22 | 14,858,905,910 | 9,285,036,692 | **-37.5%** |
14061428
| `huge_cpp` peak RSS, exp21 through exp22 | 696 MB | 491 MB | **-29%** |
1407-
| focused `typing`, exp22 through exp45 | 3,115,140,742 | 2,754,519,861 | **-11.58%** |
1408-
| focused `slow`, exp22 through exp45 | 1,881,539,982 | 514,830,042 | **-72.64%** |
1429+
| focused `typing`, exp22 through exp46 | 3,115,140,742 | 2,743,556,655 | **-11.93%** |
1430+
| focused `slow`, exp22 through exp46 | 1,881,539,982 | 514,223,778 | **-72.67%** |
14091431

14101432
The large-input pass found a different class of wins from the original suite:
14111433
quadratic display loops (exp13-17), redundant offset-to-line searches (exp19),
@@ -1414,7 +1436,7 @@ The post-exp22 `huge_cpp` profile is now led by line splitting, Imara histogram
14141436
diff construction, allocator traffic, and changed-region line conversion; the
14151437
previous hunk-end and opposite-line hash hotspots are gone.
14161438

1417-
The focused exp23-45 pass first found small, composable wins in slider range
1439+
The focused exp23-46 pass first found small, composable wins in slider range
14181440
collection and parent-stack dispatch, then a much larger win by decomposing
14191441
oversized graphs at similar sibling lists. It also ruled out three tempting
14201442
graph directions on the exact target inputs: regenerating cached neighbours,

plan.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,10 @@ new incremental experiment, use the latest accepted branch tip as the control.
241241

242242
Fresh five-run `perf stat` means at the start of the focused pass:
243243

244-
| pair | post-exp22 instructions | latest accepted after exp45 | cumulative change |
244+
| pair | post-exp22 instructions | latest accepted after exp46 | cumulative change |
245245
| --- | ---: | ---: | ---: |
246-
| `typing` | 3,115,140,742 | 2,754,519,861 | **-11.58%** |
247-
| `slow` | 1,881,539,982 | 514,830,042 | **-72.64%** |
246+
| `typing` | 3,115,140,742 | 2,743,556,655 | **-11.93%** |
247+
| `slow` | 1,881,539,982 | 514,223,778 | **-72.67%** |
248248

249249
The original focused callgrind files were `/tmp/cg-focused-typing.out` and
250250
`/tmp/cg-focused-slow.out`; sampled cycle profiles were
@@ -362,6 +362,7 @@ than mixing counts across environments.
362362
| 43 | reserve matched-position capacity from syntax-node count | flat on both focused pairs | rejected |
363363
| 44 | reuse one slider-region scratch vector across both passes | flat on both focused pairs | rejected |
364364
| 45 | replace hashed `ChangeMap` with dense syntax-ID indexing | -1.20% `typing`, -0.27% `slow`; `typing` RSS -4.5% | kept |
365+
| 46 | replace hashed content-ID counts with a dense vector | -0.40% `typing`, -0.12% `slow` | kept |
365366

366367
Every completed experiment is committed and pushed. `results/` holds labelled
367368
callgrind tables through `exp13-linear-visible-width`; experiments that only
@@ -370,8 +371,8 @@ affect inline, JSON, or synthetic/very-large shapes use repeated targeted
370371
`huge_cpp` pair from 14.86B to 9.29B instructions (**-37.5%**), and exp22 also
371372
reduced its peak RSS from 696 MB to 491 MB. If the compiler, dependencies,
372373
profiler, or machine changed, record a fresh control binary before comparing.
373-
The focused exp23-45 pass reduced `typing` from 3.115B to 2.755B (**-11.58%**)
374-
and `slow` from 1.882B to 0.515B (**-72.64%**). Exp35's similar-list pairing and
374+
The focused exp23-46 pass reduced `typing` from 3.115B to 2.744B (**-11.93%**)
375+
and `slow` from 1.882B to 0.514B (**-72.67%**). Exp35's similar-list pairing and
375376
exp39's lower decomposition gate are responsible for most of that improvement.
376377

377378
## Research synthesis: where larger wins can come from
@@ -625,9 +626,10 @@ percentages above predate exp25-31.
625626
cache and packed `VertexKey`. Look for repeated state extraction, parent
626627
stack allocation, or matching in existing edges. Exp28 and exp31 show exact
627628
parent-stack reductions pay; exp10 shows merely decomposing candidate
628-
construction does not. Exp45 also shows that dense sequential identifiers
629-
should use direct indexing rather than hashing; audit other hot maps for the
630-
same invariant before changing their representation.
629+
construction does not. Exp45-46 show that dense sequential identifiers
630+
should use direct indexing rather than hashing. The remaining content-key
631+
table is keyed by structural content and cannot use this technique; audit
632+
other hot maps only when they have the same dense invariant.
631633
8. **Target the remaining mixed-pipeline work on `typing`.** Exp27 proved
632634
capture-bucket lookup itself is negligible, while exp41 removed one
633635
allocation from `change_positions_`, and exp42 removed the per-node result
@@ -689,7 +691,7 @@ percentages above predate exp25-31.
689691
rather than comparing across machines or toolchains.
690692
5. Choose one hypothesis from the prioritised backlog, state whether it is in
691693
the exact-output or diff-quality lane, change one thing, measure both pairs,
692-
and immediately append exp46 (then exp47, etc.) to
694+
and immediately append exp47 (then exp48, etc.) to
693695
`PERF_RESEARCH_LOG.md` and the state table here.
694696
6. Fully revert rejected source changes with `apply_patch`, but commit and push
695697
their log entries. For a kept change, run the wider output oracle and full

src/parse/syntax.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,12 @@ fn init_info<'a>(lhs_roots: &[&'a Syntax<'a>], rhs_roots: &[&'a Syntax<'a>]) {
440440
set_content_id(lhs_roots, &mut existing);
441441
set_content_id(rhs_roots, &mut existing);
442442

443-
set_content_is_unique(lhs_roots);
444-
set_content_is_unique(rhs_roots);
443+
// Content IDs are dense and start at one. Keep slot zero unused so an ID
444+
// can index this vector directly.
445+
let mut content_counts = vec![0; existing.len() + 1];
446+
set_content_is_unique(lhs_roots, &mut content_counts);
447+
content_counts.fill(0);
448+
set_content_is_unique(rhs_roots, &mut content_counts);
445449
}
446450

447451
type ContentKey = (Option<String>, Option<String>, Vec<u32>, bool, bool);
@@ -532,32 +536,30 @@ fn set_identity_and_ancestry<'a>(
532536
}
533537

534538
/// Assumes that `set_content_id` has already run.
535-
fn find_nodes_with_unique_content(nodes: &[&Syntax], counts: &mut DftHashMap<ContentId, usize>) {
539+
fn find_nodes_with_unique_content(nodes: &[&Syntax], counts: &mut [usize]) {
536540
for node in nodes {
537-
*counts.entry(node.content_id()).or_insert(0) += 1;
541+
counts[node.content_id() as usize] += 1;
538542
if let List { children, .. } = node {
539543
find_nodes_with_unique_content(children, counts);
540544
}
541545
}
542546
}
543547

544-
fn set_content_is_unique_from_counts(nodes: &[&Syntax], counts: &DftHashMap<ContentId, usize>) {
548+
fn set_content_is_unique_from_counts(nodes: &[&Syntax], counts: &[usize]) {
545549
for node in nodes {
546-
let count = counts
547-
.get(&node.content_id())
548-
.expect("Count should be present");
549-
node.info().content_is_unique_to_side.set(*count == 1);
550+
node.info()
551+
.content_is_unique_to_side
552+
.set(counts[node.content_id() as usize] == 1);
550553

551554
if let List { children, .. } = node {
552555
set_content_is_unique_from_counts(children, counts);
553556
}
554557
}
555558
}
556559

557-
fn set_content_is_unique(nodes: &[&Syntax]) {
558-
let mut counts = DftHashMap::default();
559-
find_nodes_with_unique_content(nodes, &mut counts);
560-
set_content_is_unique_from_counts(nodes, &counts);
560+
fn set_content_is_unique(nodes: &[&Syntax], counts: &mut [usize]) {
561+
find_nodes_with_unique_content(nodes, counts);
562+
set_content_is_unique_from_counts(nodes, counts);
561563
}
562564

563565
/// Initialise the sibling links and the preceding node used by graph search in

0 commit comments

Comments
 (0)