Skip to content

perf(scorer): precompute log scores in NewRankScorer#16

Merged
ypriverol merged 1 commit intodevfrom
perf/precompute-log-scores
Apr 17, 2026
Merged

perf(scorer): precompute log scores in NewRankScorer#16
ypriverol merged 1 commit intodevfrom
perf/precompute-log-scores

Conversation

@ypriverol
Copy link
Copy Markdown
Member

@ypriverol ypriverol commented Apr 17, 2026

Summary

libmLog (native Math.log) showed up at 5.46 % of CPU in the Astral profile taken after PR #15 merged. The call sites in NewRankScorergetErrorScore, getNodeScore, and getMissingIonScore — compute log(x/y) over frequency arrays that are immutable after scorer load. The denominator scale factor min(ionType.getCharge(), numSegments) is also load-time constant.

This PR precomputes those logs once in precomputeLogScoreTables() at the end of readFromInputStream, stores them as float[] per (partition, …), and replaces the runtime Math.log calls with direct array indexing. Each scored edge/node saves one Math.log plus two HashMap.get calls (the NOISE lookup and one of the per-type lookups collapse into the cache).

Scoring is bit-identical: same expressions, same operand order, same float rounding; the cast to float just happens once per cell at load time. Both hot-path methods keep a fallback to the original path, so any legacy callers that populate the raw tables without going through readFromInputStream keep working.

This PR in isolation (same machine state, dev HEAD jar vs this branch)

Dataset dev HEAD branch Wall Δ RSS Δ PSM@1%FDR SII
PXD001819 LFQ (Velos CID) 122.7 s / 2410 MB 110.4 s / 2292 MB -10.0 % -4.9 % 14802 = 14802 78118 = 78118
TMT (Fusion Lumos) 295.7 s / 2793 MB 277.9 s / 2818 MB -6.0 % +0.9 % 8547 = 8547 87116 = 87116
Astral (ProteoBench M8) 1002.9 s / 7707 MB 883.5 s / 7351 MB -12.0 % -4.6 % 25037 = 25037 272542 = 272542

Cumulative impact — all three merged + pending perf PRs together (this branch vs pre-PR#15 dev)

Dataset pre-PR#15 dev this branch Wall Δ RSS Δ
PXD001819 LFQ 176.5 s / 2254 MB 110.4 s / 2292 MB -37.5 % +1.7 %
TMT 644.3 s / 2872 MB 277.9 s / 2818 MB -56.9 % -1.9 %
Astral 2155.9 s / 6775 MB 883.5 s / 7351 MB -59.0 % +8.5 %

PSM@1%FDR and SII remain bit-exact at every step. Cumulative contributions on Astral (for context):

Machine: macOS, 11 CPUs, 18 GB RAM, Java 21.0.6, -Xmx8192m (TMT/Astral) / -Xmx4096m (PXD001819), 4 threads.

Test plan

  • mvn -B verify — 141 tests pass (57 skipped for missing external data)
  • PSM@1%FDR + SII exact parity on all three full-dataset runs
  • Wall-time improves 6–12 % across all three datasets vs dev HEAD; RSS neutral or better
  • Cumulative (this branch vs pre-primitives dev): 37–59 % wall-time reduction, RSS within ±9 %

Non-goals

🤖 Generated with Claude Code

Native Math.log (libmLog) was 5.46% of CPU in the post-PR#15 Astral
profile. The call sites in NewRankScorer.getErrorScore and
getNodeScore / getMissingIonScore compute log(x/y) over frequency
arrays that are immutable after scorer load; the denominator scale
factor min(ionType.getCharge(), numSegments) is also load-time
constant. Cache the resulting float values once at the end of
readFromInputStream and replace the runtime Math.log calls with
direct array indexing.

Scoring results are bit-identical: same expressions, same operand
ordering, same float rounding; the only difference is that the cast
to float happens once per cell at load instead of per call. Both
hot-path methods keep a fallback to the original computation so
legacy callers that populate the tables without going through
readFromInputStream still work.

Benchmarks on the current machine state (baseline = dev HEAD jar,
same run, same thermal state):

  PXD001819 LFQ:  122.7s -> 110.4s (-10.0%),  2410 ->  2292 MB (-4.9%)
  TMT:            295.7s -> 277.9s ( -6.0%),  2793 ->  2818 MB (+0.9%)
  Astral:        1002.9s -> 883.5s (-12.0%),  7707 ->  7351 MB (-4.6%)

PSM@1%FDR and SII counts match exactly on all three datasets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 126afe34-3f0a-4e91-b85b-da0b4edff93e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/precompute-log-scores

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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes NewRankScorer hot-path scoring by precomputing the log-score tables at scorer load time, reducing repeated Math.log calls and repeated HashMap lookups during node/edge scoring.

Changes:

  • Added precomputed log-score caches (errorLogTable, nodeLogTable) and populated them via precomputeLogScoreTables() after parameter loading.
  • Updated getNodeScore, getMissingIonScore, and getErrorScore to use the precomputed tables with a fallback to the original computation path.
  • Hooked precomputation into readFromInputStream to ensure caches are built for the standard loading path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +480 to +492
for (Map.Entry<IonType, Float[]> ie : ionTable.entrySet()) {
IonType ionType = ie.getKey();
Float[] frequencies = ie.getValue();
if (frequencies == null) continue;
int n = Math.min(frequencies.length, noiseFrequencies.length);
int chargeOrSeg = Math.min(ionType.getCharge(), numSegments);
float[] logs = new float[n];
for (int i = 0; i < n; i++) {
float ionFrequency = frequencies[i];
float noiseFrequency = noiseFrequencies[i] * chargeOrSeg;
// Match getScoreFromTable semantics exactly: guard against non-positive only in assertions.
logs[i] = (float) Math.log(ionFrequency / noiseFrequency);
}
@ypriverol ypriverol merged commit 9cdae16 into dev Apr 17, 2026
6 checks passed
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.

2 participants