Skip to content

perf(spectrum): combine SA position+key into one entry() read in the compare loop - #37

Merged
nh13 merged 1 commit into
mainfrom
feat/v0.3-sa-entry
Jun 13, 2026
Merged

perf(spectrum): combine SA position+key into one entry() read in the compare loop#37
nh13 merged 1 commit into
mainfrom
feat/v0.3-sa-entry

Conversation

@nh13

@nh13 nh13 commented Jun 13, 2026

Copy link
Copy Markdown

v0.3 perf series — the last net-new alloc/CPU trim (17d506b). Closes out PR-C (C-alloc).

What

Every keyed-compare probe read the SA entry twicesa_position_for(i) then key_at(i) — each recomputing the entry offset (header + i*bytes_per_entry) and re-running the bounds assert, on the same 13-byte entry / one cache line. This adds SaFileReader::entry(i) -> (position, Option<key>) + LearnedIndex::sa_entry and uses it at every per-probe site (the forward/backward boundary loops, push_unique_suffix_tail, the hint paths, lcp_at, ref_less/shares_prefix, the lockstep stepper) — one bounds check + one offset computation per probe instead of two.

Honest scope

The probe loop is memory-latency-bound (the cold entry read dominates, prefetch overlaps it), so this trims instruction count without moving wall time much — but it is the hottest loop, and unlike the deferred LCP-accel (#36) it has no amortization question: it's strictly fewer ops per probe, never more. Net-positive or neutral, never a regression.

Byte-identity

entry(i) returns exactly (position(i), key_at(i)) — proven by the new entry_equals_position_and_key_at round-trip test (modes 1+2). Each of the 15 call sites binds the same (pos, key) from one combined read; mode-1 still yields key = None; the explicit bump_probe() per site is preserved (probe count unchanged). The existing mem_search/spectrum/collect byte-identity oracle proptests pass unchanged. On-disk format unchanged.

Series status

With this, the v0.3 perf carve is essentially complete — the only remaining item is the deferred LCP-accel wiring (#36), parked because it's net-neutral-to-negative for the latency-bound consumer (its foundation #35 stands as a ready primitive).

Pre-PR /coderabbitai-review: 0 findings.

Summary by CodeRabbit

Release Notes

  • Refactor

    • Consolidated internal data retrieval operations for improved efficiency across search operations.
    • Optimized access patterns to reduce redundant reads within spectrum and boundary search paths.
    • Streamlined probe instrumentation in affected code paths.
  • Tests

    • Added test coverage for consolidated data retrieval functionality across file modes.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nh13, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 44 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11b533e9-d7c6-4344-b260-504b2bc7d6ff

📥 Commits

Reviewing files that changed from the base of the PR and between 364c14f and 7e7cae8.

📒 Files selected for processing (4)
  • prmi/src/index/mod.rs
  • prmi/src/index/spectrum.rs
  • prmi/src/sidecar/sa_file.rs
  • prmi/tests/sidecar_sa_file.rs
📝 Walkthrough

Walkthrough

This PR consolidates suffix array entry retrieval by adding a combined zero-copy read API, exposing it through the index layer, and refactoring all major spectrum and boundary-search code paths to use the new API instead of separate position and key lookups.

Changes

SA Entry Consolidation

Layer / File(s) Summary
SaFileReader entry() foundation
prmi/src/sidecar/sa_file.rs
SaFileReader::entry() performs a single zero-copy read of SA entry's 5-byte packed position and conditional 8-byte key, computing one base offset and conditionally unpacking both fields.
LearnedIndex wrapper
prmi/src/index/mod.rs
LearnedIndex::sa_entry() delegates to SA file reader's entry method as a hot-path equivalent to separate sa_position_for() and key_at() calls.
Forward spectrum hot loops
prmi/src/index/spectrum.rs
forward_spectrum_into and forward_spectrum_tabled_into refactored to use sa_entry(mid) in binary search lower/upper bounds, with probe bumps positioned immediately before the combined read.
Boundary search predicates
prmi/src/index/spectrum.rs
Helper predicates (ref_less, shares_prefix, lcp_at) and bound functions (lower_bound_prefix, upper_bound_prefix) consolidate SA reads via sa_entry(mid) while maintaining LCP-based logic.
Hint-based search paths
prmi/src/index/spectrum.rs
forward_spectrum_from_hint and mem_search_from_hint updated to fetch (pos, key) via sa_entry(hint) instead of separate reads.
Backward search paths
prmi/src/index/spectrum.rs
backward_spectrum_via_stepper and backward_spectrum_lockstep refactored to use sa_entry(mid) for stepper advancement and per-round batch loads.
Utility functions
prmi/src/index/spectrum.rs
push_unique_suffix_tail and lcp_query_suffix updated to use sa_entry() for combined position/key retrieval.
Entry consistency test
prmi/tests/sidecar_sa_file.rs
Test entry_equals_position_and_key_at verifies SaFileReader::entry(i) matches (position(i), key_at(i)) across Mode 2 and Mode 1 file variants.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • fg-labs/prmi#21: Both PRs modify hot spectrum/boundary-search loops in prmi/src/index/spectrum.rs to change SA entry access patterns; main consolidates (sa_position_for, key_at) into sa_entry(pos,key) while retrieved adds SA prefetching around those same probes.
  • fg-labs/prmi#31: Both PRs modify prefix-probing helpers (shares_prefix, ref_less) in prmi/src/index/spectrum.rs; main refactors these to use the new combined sa_entry read that the retrieved PR's forward_truncate_below_maximal implementation relies on.
  • fg-labs/prmi#20: Both PRs touch hinted spectrum hot paths in prmi/src/index/spectrum.rs; #20 adds probe-counting/FFI around hinted search while main refactors those same SA reads to use combined sa_entry(..) with bump_probe() repositioned immediately before it.

Poem

🐇 One read where two once trod,
Spectrum searches now find the path less broad,
Position and key, a pair now joined,
Through boundary and backward, all paths aligned,
The refactor hops on!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'perf(spectrum): combine SA position+key into one entry() read in the compare loop' accurately summarizes the main change: combining two separate SA reads (position and key) into a single optimized entry() read for performance.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nh13

nh13 commented Jun 13, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
prmi/tests/sidecar_sa_file.rs (1)

172-184: ⚡ Quick win

Use key_at(i) in the mode-1 equivalence assertion to keep the contract exact.

At Line 183, asserting against None is correct today, but the test’s stated purpose is API equivalence. Comparing with r1.key_at(i) keeps that invariant explicit and symmetric with mode 2.

Suggested test tweak
-    for i in 0..r1.num_entries() {
-        assert_eq!(r1.entry(i), (r1.position(i), None), "entry({i}) mode 1");
-    }
+    for i in 0..r1.num_entries() {
+        assert_eq!(
+            r1.entry(i),
+            (r1.position(i), r1.key_at(i)),
+            "entry({i}) != (position, key_at) in mode 1"
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prmi/tests/sidecar_sa_file.rs` around lines 172 - 184, The test asserts
mode-1 entries equal (r1.position(i), None) which is correct today but should
use the API symmetry: replace the literal None with r1.key_at(i) so the
assertion reads assert_eq!(r1.entry(i), (r1.position(i), r1.key_at(i)),
"entry({i}) mode 1"); update the loop in the test that iterates over
r1.num_entries() and uses r1.entry(i) and r1.position(i) to use r1.key_at(i)
instead of None to keep the contract exact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@prmi/tests/sidecar_sa_file.rs`:
- Around line 172-184: The test asserts mode-1 entries equal (r1.position(i),
None) which is correct today but should use the API symmetry: replace the
literal None with r1.key_at(i) so the assertion reads assert_eq!(r1.entry(i),
(r1.position(i), r1.key_at(i)), "entry({i}) mode 1"); update the loop in the
test that iterates over r1.num_entries() and uses r1.entry(i) and r1.position(i)
to use r1.key_at(i) instead of None to keep the contract exact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b346c30f-8d98-4e4a-bf1d-133d950506be

📥 Commits

Reviewing files that changed from the base of the PR and between 40aacdc and 364c14f.

📒 Files selected for processing (4)
  • prmi/src/index/mod.rs
  • prmi/src/index/spectrum.rs
  • prmi/src/sidecar/sa_file.rs
  • prmi/tests/sidecar_sa_file.rs

…compare loop

Every keyed-compare probe read the SA entry twice — sa_position_for(i) then
key_at(i) — each recomputing the entry offset (header + i*bytes_per_entry) and
re-running the bounds assert, on the same 13-byte entry (one cache line). Add
SaFileReader::entry(i) -> (position, Option<key>) and LearnedIndex::sa_entry, and
use it at every per-probe site (the forward/backward boundary loops,
push_unique_suffix_tail, the hint paths, lcp_at, ref_less/shares_prefix, and the
lockstep stepper).

One bounds check and one offset computation per probe instead of two. The probe
loop is memory-latency-bound (the cold entry read dominates and prefetch overlaps
it), so this trims instruction count without moving wall time much — but it is the
hottest loop. Byte-identical: entry(i) returns exactly (position(i), key_at(i))
(new entry_equals_position_and_key_at round-trip test, modes 1+2), the explicit
bump_probe() per site is preserved (probe count unchanged), and the existing
mem_search/spectrum/collect byte-identity oracle proptests pass unchanged.
On-disk format unchanged.
@nh13
nh13 force-pushed the feat/v0.3-sa-entry branch from 364c14f to 7e7cae8 Compare June 13, 2026 21:10
@nh13
nh13 merged commit 7da93a5 into main Jun 13, 2026
4 checks passed
@nh13
nh13 deleted the feat/v0.3-sa-entry branch June 13, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant