Skip to content

refactor(cpp): ♻️ extract the row hash table from OperatorIndex - #303

Open
robertodr wants to merge 1 commit into
split/01-baseline-toolfrom
split/02-row-hash-table
Open

refactor(cpp): ♻️ extract the row hash table from OperatorIndex#303
robertodr wants to merge 1 commit into
split/01-baseline-toolfrom
split/02-row-hash-table

Conversation

@robertodr

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Stacked on #302 — review that first; this PR's diff against it is RowHashTable.h plus the OperatorIndex rewrite.

Summary

Second of five PRs carved out of #226. OperatorIndex carries two things that have nothing to do with each other: a keyless open-addressing hash table, and the packed row storage the table indexes into. A second row backend is coming, and it needs the table and not the rows, so the table moves out first, on its own, with no behaviour change.

RowHashTable is the table alone — power-of-two slots, linear probing, load factor 0.7, a 32-bit folded hash kept beside each slot as an equality pre-filter. It stores no keys: hashing, equality and prefetch arrive as callables from the owner, which is what lets a caller key its rows in whatever form it already has them in. OperatorIndex keeps its own API and forwards to it.

This is deliberately inert. The table's slot layout fixes for_each_slot order, which fixes evolved-term order, which fixes floating-point accumulation order — so a reordering here would move energies. Verified against main's implementation over 4000 random monomials: identical iteration order, find / find_batch results, clone order and memory_bytes.

Changes

  • cpp/monoprop/detail/operator/RowHashTable.h: new. The table, TermIndexCeilingReached, kIndexCeiling, fold, find / find_batch / emplace / insert_distinct / insert_distinct_range, for_each_slot, reserve, slot_bytes.
  • cpp/monoprop/detail/operator/OperatorIndex.h: keeps the rows, delegates the table. -195 lines.
  • cpp/monoprop/detail/operator/CMakeLists.txt: list the new header.

Checklist

  • Tests added or updated to cover the changes
  • Documentation updated (docstrings, docs/, CONTRIBUTING.md) if needed
  • CHANGELOG / release notes updated if applicable

Covered by the existing operator_index_tests.cpp; the gate for a change of this kind is just diff-baseline (#302), which must come out byte-identical.

AI/LLM disclosure

  • I used the following tool to help write this PR description: ClaudeCode (claude-opus-5)
  • I used the following tool to generate or modify code: ClaudeCode (claude-opus-5)

@github-actions

Copy link
Copy Markdown

Docs preview: https://pr-303.monoprop-docs.pages.dev

@robertodr
robertodr force-pushed the split/02-row-hash-table branch from 403debb to 8ded3bd Compare August 29, 2026 14:35
@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.70%. Comparing base (fb9454e) to head (e4b1217).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                   Coverage Diff                   @@
##           split/01-baseline-tool     #303   +/-   ##
=======================================================
  Coverage                   97.70%   97.70%           
=======================================================
  Files                          14       14           
  Lines                         742      742           
  Branches                       98       98           
=======================================================
  Hits                          725      725           
  Misses                         12       12           
  Partials                        5        5           
Flag Coverage Δ
cpp 97.70% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

OperatorIndex was two things: a packed position-list row representation and a
keyless open-addressing index over those rows. RowHashTable is the second half,
lifted out whole -- power-of-2 slots, linear probing, load factor 0.7, a 32-bit
folded hash per slot used only as an equality pre-filter.

Keyless is what makes the split possible: the table never stores or compares a
key, so the hash and the equality test arrive as callables and the row
representation stays entirely on the caller's side. find_batch keeps its
pipeline by taking the row prefetch as a third callable -- deferring
confirmation past the probe is the whole reason that prefetch has somewhere to
go.

The table's slot layout fixes for_each_slot's iteration order, which is the
order of a propagator's evolved-term list and therefore its floating-point
accumulation order, so this had to come out inert. Checked byte-wise against
the previous implementation over 4000 random monomials: identical iteration
order, find and find_batch results, clone order and memory_bytes.

Assisted-by: ClaudeCode:claude-opus-5
@robertodr
robertodr force-pushed the split/02-row-hash-table branch from 8ded3bd to e4b1217 Compare August 29, 2026 14:59
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant