Skip to content

perf: back frequency counters and hot lookups with fxhash instead of SipHash - #48

Merged
tfenne merged 3 commits into
mainfrom
tf_fxhash
Jul 4, 2026
Merged

perf: back frequency counters and hot lookups with fxhash instead of SipHash#48
tfenne merged 3 commits into
mainfrom
tf_fxhash

Conversation

@tfenne

@tfenne tfenne commented Jul 4, 2026

Copy link
Copy Markdown
Member

What

Swaps std's default SipHash for fxhash (rustc-hash, already a dependency) wherever hash maps/sets sit on high-volume paths:

  • Counter<K> (src/counter.rs) — the frequency counter behind the isize/alignment/rna histograms; .count() runs on the per-record hot path.
  • rna::ignored_ref_ids — a HashSet<usize> probed on every record in accept().
  • sequence_dict::name_to_index — setup-only today, but a foundational contig-name lookup that could land on a hot path later.
  • gene_model GFF/GTF parsersHashMap<String, …> keyed by feature/transcript/gene id, one insert per annotation line (millions for a full GENCODE). One-time at startup, not per-record, but a free shave off gene-model load.

Output is byte-identical; all 771 tests pass.

Why (measured, not guessed)

std SipHash is DoS-resistant but ~4–5× slower per op than fxhash, and none of these keys are attacker-controlled. Microbenchmarked at the real HG03953.2× record counts:

histogram SipHash fxhash
isize |TLEN| (22.7M) 6.73 ns/op 1.34 ns/op (5.0×)
alignment adjusted-NM (45M) 6.66 ns/op 1.51 ns/op (4.4×)

For the Counter swap that's ~0.7–1.4% of alignment/isize wall-clock (more at higher thread counts, where decode is offloaded and the denominator shrinks) — byte-identical.

The gene_model maps are drained into sorted output, so iteration order never mattered (std's per-run SipHash randomization already guaranteed that); fxhash is a safe drop-in.

What was deliberately NOT done

A dense-array + overflow Counter (array index instead of any hash) was prototyped and profiled. It's a further 1.4–3.3× faster per count(), but worth only ~0.1–0.2% of runtime: both commands are decode-bound (libdeflate is 46–76% of samples; even multi --threads 3 amortized over three counter-using tools keeps Counter::count at 0.2%). It failed the "only optimize hot paths" bar and would have added a CounterKey trait + dense/overflow machinery for no measurable gain, so it was dropped.

Sweep coverage

Every other std HashMap/HashSet in src/ is cold (per-contig transitions, finish()-scoped scans, or one-time init) and left on the default.

tfenne added 2 commits July 4, 2026 13:18
Counter<K> sits on the hot per-record path — every alignment/isize/rna
record calls .count(). std's default SipHash is cryptographically strong
but slow, and the counter keys are small non-adversarial integers, so
fxhash (rustc-hash, already a dependency — error.rs uses it) is the right
trade. This is a backing-store swap only; output is byte-identical (all
exact-output integration tests pass unchanged).

Step 1 of the Counter redesign; a dense+overflow hybrid follows once
profiling sizes its marginal gain.
A sweep for std SipHash HashMap/HashSet on high-volume paths (same
motivation as the Counter fxhash swap) turned up two worth changing:

- rna: `ignored_ref_ids` is probed per record in `accept()` to divert
  ignore-list reads -- the one genuinely per-record std HashSet.
- sequence_dict: `name_to_index` is setup-only today, but it's a
  foundational contig-name lookup that could land on a hot path later,
  so default it to a non-cryptographic hasher now.

Everything else on std SipHash is cold (per-contig transitions,
finish()-scoped scans, or one-time annotation parsing) and left as-is.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92d2311a-0946-42a6-b49a-ce546afaa6ea

📥 Commits

Reviewing files that changed from the base of the PR and between dc8dfa4 and 5506488.

📒 Files selected for processing (6)
  • src/commands/rna.rs
  • src/counter.rs
  • src/gene_model/gff.rs
  • src/gene_model/gtf.rs
  • src/gene_model/mod.rs
  • src/sequence_dict.rs

📝 Walkthrough

Walkthrough

Internal hash-based collections across the codebase are switched from Rust's standard HashMap/HashSet to rustc_hash::FxHashMap/FxHashSet. Affected: Counter<K>'s map, gene model parsing accumulators in gff.rs, gtf.rs, and mod.rs, SequenceDictionary's name_to_index field, and RnaCollector's ignored_ref_ids set. Imports and constructor calls (HashMap::new()/HashSet::new()FxHashMap::default()/FxHashSet::default()) are updated accordingly. No public API signatures or logic behavior changed beyond field types.

Changes

Area Change
src/counter.rs Counter map field switched to FxHashMap
src/gene_model/gff.rs Feature/exon/CDS/seqid maps switched to FxHashMap
src/gene_model/gtf.rs by_tx accumulator switched to FxHashMap
src/gene_model/mod.rs by_gene accumulator switched to FxHashMap
src/sequence_dict.rs name_to_index field switched to FxHashMap
src/commands/rna.rs ignored_ref_ids field switched to FxHashSet

Related Issues: None referenced.

Related PRs: None referenced.

Suggested labels: performance, refactor

Suggested reviewers: None specified.

Poem
A hash by any speedier name,
FxHash now runs the same game.
Maps and sets, swapped with care,
Counter, dict, and RNA share.
No logic bent, just faster still—
a rabbit hops with quicker will. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarizes the main change: swapping SipHash-backed collections to fxhash on hot paths.
Description check ✅ Passed It accurately describes the fxhash swaps, rationale, and scope of the performance changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tf_fxhash

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.

The GFF/GTF parsers and the shared transcript->gene grouping build
HashMap<String, ...> keyed by feature/transcript/gene ids -- one insert
per annotation line, millions for a full GENCODE. It's a one-time
startup cost (not per-record), but swapping std SipHash for fxhash
shaves a little off gene-model load for free.

Output is byte-identical: the maps are drained into sorted output, so
iteration order never mattered (std's per-run SipHash randomization
already guaranteed that).
@tfenne

tfenne commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 4, 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.

@tfenne
tfenne merged commit 0bc851f into main Jul 4, 2026
5 checks passed
@tfenne
tfenne deleted the tf_fxhash branch July 4, 2026 20:49
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