Skip to content

perf(algebraic): reduce exact cardinality overhead - #505

Closed
ajroetker wants to merge 5 commits into
mainfrom
perf/algebraic-cardinality
Closed

ajroetker wants to merge 5 commits into
mainfrom
perf/algebraic-cardinality

Conversation

@ajroetker

Copy link
Copy Markdown
Contributor

What changed

  • uses hash-set membership for exact doc-id constraints
  • makes child cardinality evaluation single-pass
  • removes row scaffolding and unused helpers
  • accumulates additive results natively

Why

These PR #141 optimizations are result-preserving and independent of HLL, adaptive materialization, relational storage, and the larger #145 integration.

Impact

Exact algebraic cardinality paths allocate less and avoid repeated traversal while retaining canonical results.

Validation

  • 3 focused algebraic cardinality/fold/distributed-merge tests passed, no leaks
  • git diff --check

claude added 5 commits August 18, 2026 14:37
…raints

Cardinality, range, and histogram queries funnel through
cardinalityDocMatchesConstraints, a linear scan over a doc-id slice that
is invoked once per scanned index entry. For histogram/range children the
full field prefix is re-scanned per bucket and tested against that
bucket's doc-id list, making the scan O(entries * constraints) -- i.e.
quadratic in the document count. These were the only algebraic queries
measured slower than a plain document scan in the algebraic benchmark.

Introduce DocIdConstraintSet, a borrowed-key hash-set built once per scan,
and replace the linear membership test at all eight cardinality
scan/collect sites. Each membership test becomes O(1) and the scan is a
single linear pass again. Results are unchanged (the benchmark
cross-checks algebraic output against document-scan checksums).

https://claude.ai/code/session_01UVKuQU8S6THBNkSBHoVnzz
Histogram, range, and terms cardinality queries computed each bucket's
nested child cardinality by re-scanning the entire child field once per
bucket (scanDistributedCardinalityPartialsForDocIds), making the cost
O(buckets * field_entries).

Build a ChildCardinalityIndex once per child field (a single cursor scan
grouping distinct value-keys by document) and look up each bucket's
documents against it, turning the work into O(field_entries +
bucket_members). Output partials are byte-identical, so cross-engine
correctness checksums are unchanged.

Quick profile (1000 docs), algebraic engine avg latency:
  histogram_nested_cardinality  15.2ms -> 5.4ms
  range_nested_cardinality      14.3ms -> 10.9ms
  root_cardinality               5.6ms -> 3.5ms
mergeOneSlotValuesAlloc wrapped each fold/merge contribution in two
throwaway single-slot Rows (two row allocations, two value dupes, a
combine, and a final dupe) just to call law.combineAlloc on one slot.
On the per-contribution fold and distributed-merge hot paths this is
~6 allocations for what is a single law combine.

law.combineAlloc already returns a freshly owned buffer, so call it
directly and fall back to the law identity when it yields null
(preserving the previous "never null" contract). Output bytes are
unchanged: every algebraic benchmark checksum is identical before and
after.
DerivedJoinFoldAccumulator round-tripped each group's running total
through decimal text on every contribution: combineAlloc parsed the
accumulated text and the contribution, added, and re-formatted, once per
folded row.

Hold the running value natively instead - i64 for count, f64 for
sum/sumsquares, and {sum,count} for avg - and format to text only once
in entriesAlloc. encode/parse round-trips exactly for i64 and f64 ("{d}")
and contributions are applied in the same order, so the formatted result
is byte-identical (every algebraic benchmark checksum is unchanged). The
non-additive laws (min/max, booleans, set/tuple union, max-timestamp)
keep the text-merge path, where original-token text and order matter.
The linear doc-id membership helper has no callers since the cardinality
scans switched to DocIdConstraintSet hash-set membership.
@ajroetker

Copy link
Copy Markdown
Contributor Author

Superseded by #509, which consolidates the extracted cardinality and dynamic-template algebraic work into one PR.

@ajroetker ajroetker closed this Aug 18, 2026
@ajroetker
ajroetker deleted the perf/algebraic-cardinality branch August 18, 2026 23:56
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