Skip to content

perf: inlinable bit helpers, zeroOut skip, and accumulator window clamp - #55

Merged
aliszka merged 4 commits into
mainfrom
feat/bitmap-mask-helpers
Aug 31, 2026
Merged

aliszka merged 4 commits into
mainfrom
feat/bitmap-mask-helpers

Conversation

@aliszka

@aliszka aliszka commented Jul 31, 2026

Copy link
Copy Markdown
Member

Motivation

Three independent, behavior-preserving optimizations to hot paths in the sroar bitmap library: per-bit index/mask arithmetic, the unconditional 8KB clear in zeroOutContainer, and the Accumulator's whole-block popcount-and-copy passes. No exported API or observable behavior changes.

Approach

Bit helpers replace the bitmapMask table. bitmapMask becomes a function (0x8000 >> pos) — one shift instead of a table load plus bounds check. New setBit/clearBit/hasBit compose it, replacing ~10 open-coded index/mask sites across container*.go, accumulator.go, bitmap_opt.go. Fixed-word-position loops and uint64 composition keep bare bitmapMask(pos). bitmap.add sat at the inlining budget, so the mask is hoisted to a local to keep it inlined into Set.

zeroOutContainer skips the clear when already empty. A zero-cardinality header implies zero payload — every writer that zeroes the header zeroes the payload — so the memset is wasted on empty containers. And-style folds that re-zero the same emptied containers each pass: 5.9µs → 146ns per ZeroOut over 100 empty containers. Non-empty paths unchanged.

Accumulator build clamps to each block's touched-word window. layout previously popcounted all 4096 words of every staging block and buildInto walked them again; for clustered doc-ID runs ~98% covered provably zero words. layout now records each block's touched window (blockLayout{card,min,max}, same 8 bytes as the old cards slice), and buildInto extracts array containers from that window only via an offset-aware bitsIntoArray. Dense blocks keep whole copies.

Key areas for review

  • container*.go — the helper substitution is mechanical; confirm the sites kept on bare bitmapMask(pos) are position-indexed, not value-indexed.
  • zeroOutContainer — the skip rests on "card 0 ⇒ payload 0", already load-bearing for reads (all()/ToArray ignore the header); writers were traced.
  • accumulator.go:layout/buildInto — prefix/suffix zero-scan split from the popcount (each word visited once); bitsIntoArray's new baseWord offset stays overrun-safe against an undersized output.

Testing

New equivalence guards (bitmapMask round-trips, set/clear/has, interior window extraction, repeated-zeroOut invariant) extend the existing tables. Full suite green under -race; go vet / golangci-lint clean on changed lines. Interleaved A/B: bit helpers geomean flat (+0.03%), accumulator -46% clustered / -5.5% uniform sparse / flat on dense. Allocations identical across all three.

@orca-security-eu orca-security-eu 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.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca

@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch 2 times, most recently from 00d3bed to d1a1c82 Compare August 3, 2026 11:30
@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch from d1a1c82 to cadbdf5 Compare August 17, 2026 21:56
@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch from cadbdf5 to 9daeec8 Compare August 18, 2026 13:13
@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch from 9daeec8 to 561cc38 Compare August 18, 2026 16:19
@amourao
amourao requested a lite review from Copilot August 19, 2026 08:27

Copilot AI 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.

Pull request overview

This PR applies three behavior-preserving performance optimizations to hot paths in the sroar bitmap library: replacing a lookup-table bit mask with inlinable helpers, skipping redundant clears when containers are already empty, and narrowing Accumulator build work to the touched-word window per staging block.

Changes:

  • Replaced the bitmapMask table with an inlinable bitmapMask(pos) function plus setBit/clearBit/hasBit helpers, updating call sites across container/bitmap/accumulator code.
  • Optimized zeroOutContainer to return early when the container is already empty, avoiding unnecessary 8KB clears for bitmap payloads.
  • Updated Accumulator.layout/buildInto to record and use a per-block touched-word window, reducing popcount/copy work for sparse clustered blocks; updated tests/benchmarks accordingly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
container.go Adds bit helpers, converts mask table to function, updates bit operations, and skips zeroOutContainer clear when already empty.
container_opt.go Switches optimized container operations to use the new bit helpers and functional bitmapMask.
bitmap_test.go Adds an invariant test for repeated ZeroOut() with follow-up merge to surface stale payload bits.
bitmap_opt.go Updates bitmap fill/set-range paths to use the functional bitmapMask / setBit.
benchmark_accumulator_test.go Adds a clustered “tiny” benchmark to measure the Accumulator touched-window optimization.
accumulator.go Uses bit helpers and introduces blockLayout + touched-word window extraction for faster builds.
accumulator_test.go Adds a regression test for interior touched-window extraction correctness.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread container.go Outdated
Comment on lines +96 to +99
// zeroOutContainer empties c. A zero cardinality header implies the payload
// is already zero — every writer that zeroes the header also zeroes the
// payload — so the 8KB clear is skipped then (And-style passes re-zero the
// same emptied containers repeatedly).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed — the claim now scopes to bitmap payloads. Arrays do keep stale values past a zero cardinality, as zeroOutContainer itself does.

Comment thread accumulator.go Outdated
Comment on lines +572 to +574
// Splitting the prefix/suffix zero-scan from the popcount beats
// tracking the window inside the popcount loop, which would tax every
// word of every block. Each word is still visited only once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Right — it's N+2 reads per non-empty block, since the two boundary words are read by both a scan and the popcount. Reworded to say that.

Comment thread container.go Outdated
// payload — so the 8KB clear is skipped then (And-style passes re-zero the
// same emptied containers repeatedly).
func zeroOutContainer(c []uint16) {
if isEmpty(c) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: The isEmpty guard inside zeroOutContainer un-inlines (*Bitmap).ZeroOut (measured: inline cost 70 → 85, past the 80 budget), losing inlining at four And/AndNot fast-path call sites the base branch had — a real regression in a perf PR. Moving the guard to the three repeat-zeroing call sites instead was verified to restore cost 70 with tests green, and it also restores the old self-healing scrub for corrupt FromBuffer-adopted containers (the one correctness-class finding, which is defense-in-depth only).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed: cost 85 against the 80 budget, and the four And-family fast paths lose inlining. Fixed by scoping the guard to bitmaps and writing the test out instead of calling isEmpty — that's 15 cheaper and puts ZeroOut at exactly 80, so the skip and the inlining both survive; noted in the comment that there's no headroom left.

Comment thread accumulator.go Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: buildInto's bitmap branch ignores the window it just computed — it copies the full 8KB block where the windowed copy is strictly no worse and pays off exactly on the dense-clustered shape the PR's benchmark motivates (the added fixture never produces bitmap-typed blocks, so this shape is unmeasured).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Taken, with clears either side of the window: for non-zero keys the destination comes from newContainerNoClr, so the full copy was doubling as the scrub for dirty pooled memory. Windowed is ~60% faster at the clustered window this shape actually produces (188 of 4096 words) and a wash at full width — regression test added, since the natural one passes even with the clears removed.

Comment thread container.go
// is already zero — every writer that zeroes the header also zeroes the
// payload — so the 8KB clear is skipped then (And-style passes re-zero the
// same emptied containers repeatedly).
func zeroOutContainer(c []uint16) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: The new early return can leave an imported bitmap non-empty. FromBufferWithCopy accepts a zero-cardinality header with a nonzero bitmap payload; after ZeroOut and Set(1), HEAD reports cardinality 1 while Contains(0) and Contains(1) are true and ToArray is [0]. Could we either keep clearing bitmap payloads here or validate and canonicalize every FromBuffer input, then add this exported-API sequence as a regression test?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reproduced, but I couldn't find an in-library producer: no normal operation yields a zero-cardinality bitmap header over a nonzero payload, and neither does container creation over dirty memory — newContainer clears, scootRight zeroes the gap it opens, and expandContainer fully overwrites on the array→bitmap conversion. It needs a buffer sroar didn't write, and FromBuffer validates no other header field either, so I'd rather not special-case this one.

@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch 3 times, most recently from f15b81a to 9e2eb77 Compare August 25, 2026 13:44
@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch from 9e2eb77 to d2d7cff Compare August 25, 2026 13:52
@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch from d2d7cff to 39cb721 Compare August 31, 2026 12:05
Base automatically changed from feat/accumulator-ops to main August 31, 2026 12:08
aliszka and others added 4 commits August 31, 2026 14:08
bitmapMask becomes a function (0x8000 >> pos) — one shift instead of a
table load plus bounds check, ~40% faster on the isolated deposit loop —
and setBit/clearBit/hasBit compose it for the value-indexed sites, so
deposit and merge loops state their intent instead of open-coding
index/mask arithmetic. Fixed-word position loops (removeRange, rank,
selectAt) and the uint64 mask composition keep bare bitmapMask(pos);
orArray's popcount-diff counting branch keeps its branchless form.

bitmap.add sat at exactly the inlining budget (cost 80) and the helper
calls pushed it over, un-inlining it from Set (+2.25% measured); hoisting
the mask into a local restores it (cost 77). Interleaved A/B benchmarks
against the previous commit are statistically flat (geomean +0.03%),
allocations identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A zero cardinality header implies the payload is zero — every writer that
zeroes the header zeroes the payload with it — so the unconditional 8KB
memset is wasted on containers that are already empty. And-style passes
(AndConc under repeated ContainsAll folds, ZeroOut on pooled bitmaps)
re-zero the same emptied containers every pass: measured 5.9µs -> 146ns
per ZeroOut over 100 already-empty bitmap containers. Non-empty paths are
unaffected (interleaved A/B flat).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
layout previously popcounted all 4096 words of every staging block and
buildInto walked them all again, so finalize cost tracked blocks, not
content — for clustered doc-ID runs ~98% of both passes covered provably
zero words. layout now records each block's touched window alongside its
cardinality (blockLayout{card,min,max} replaces the cards slice at the
same 8 bytes per entry) and buildInto extracts array containers from that
window only, via an offset-aware bitsIntoArray. Dense copies stay whole:
blocks that finalize as bitmap containers have near-full windows anyway.

The scan visits every word exactly once — zero-test the untouched prefix
and suffix in from both ends, popcount only the window between — a shape
suggested by Andrzej after in-loop min/max tracking measured +3.4% on
dense blocks. Interleaved A/B vs the previous commit: -46% on clustered
sequential unions, -5.5% uniform sparse, flat on dense and few-block
shapes, allocations identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…copy

The isEmpty guard in zeroOutContainer put ZeroOut at inline cost 85 against a
budget of 80, so the And/AndConc/AndMasked/AndMaskedConc fast paths stopped
inlining it — a regression in a perf change. Scoping the guard to bitmaps,
where the clear is, and writing the test out instead of calling isEmpty costs
15 less and lands ZeroOut on exactly 80, keeping both the skip and the
inlining.

buildInto's bitmap branch computed a touched-word window and then copied the
whole 8KB block. It now copies just the window and clears either side, which
is ~60% faster on the clustered blocks this shape produces and a wash when the
window spans the block. The clears are required, not incidental: for non-zero
keys the container comes from newContainerNoClr, so the full copy had been
doubling as the scrub for dirty pooled memory. The added test places values
mid-block and inspects the payload, since a build that skips the clears still
passes a ToArray comparison.

Also corrects two comments: a zero cardinality implies a zeroed payload only
for bitmaps, and the layout scan reads each block's two boundary words twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aliszka
aliszka force-pushed the feat/bitmap-mask-helpers branch from 39cb721 to 9c7ca5a Compare August 31, 2026 12:08

@orca-security-eu orca-security-eu 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.

Orca Security Scan Summary

Status Check Issues by priority
Passed Passed Infrastructure as Code high 0   medium 0   low 0   info 0 View in Orca
Passed Passed SAST high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Secrets high 0   medium 0   low 0   info 0 View in Orca
Passed Passed Vulnerabilities high 0   medium 0   low 0   info 0 View in Orca

@aliszka
aliszka merged commit 4ceb958 into main Aug 31, 2026
6 checks passed
@aliszka
aliszka deleted the feat/bitmap-mask-helpers branch August 31, 2026 12:17
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.

4 participants