Skip to content

Decode whole words in bulk in bitmapContainer's many-iterator - #560

Merged
lemire merged 1 commit into
vbmi2-toarrayfrom
vbmi2-nextmany
Aug 27, 2026
Merged

Decode whole words in bulk in bitmapContainer's many-iterator#560
lemire merged 1 commit into
vbmi2-toarrayfrom
vbmi2-nextmany

Conversation

@lemire

@lemire lemire commented Aug 27, 2026

Copy link
Copy Markdown
Member

Description

NextMany on a dense bitmap walks set bits one at a time. This decodes whole words in bulk with the AVX-512 kernel added in #557.

Stacked on #557 - it reuses fillLeastSignificant16bitsVector and the useVectorFill gate from that PR, so the base branch is vbmi2-toarray, not master. Review #557 first; the diff here is just bitmapcontainer.go plus a test.

Type of Change

  • Performance improvement

Changes Made

bitmapContainerManyIterator.nextMany keeps its word-at-a-time loop for the partially consumed word, but when the current word is exhausted it now decodes whole words in bulk for as long as the caller's buffer has room for the 64 values a single word could produce. bitset stays zero across the bulk step, preserving the iterator's invariant that word base has been fully consumed, so the scalar loop picks up correctly afterwards and the saved state is unchanged in shape.

The word count per bulk step is room / 64, which cannot overflow the buffer whatever the data looks like. With a 4096-entry buffer that is 64 words per step, so a full container takes a handful of steps.

nextMany64 is deliberately not changed. Its output is []uint64, which would need a byte-to-quadword widen - eight 64-byte stores per input word instead of two - and that is a different kernel with a different trade-off. Worth measuring separately rather than assuming it carries over.

Performance Impact

Xeon Gold 6548N (Emerald Rapids), pinned with taskset, Go 1.26.3.

Iterating a whole bitmap container, ns per value:

container cardinality buffer master this PR
4096 4096 1.240 0.418 2.97x
16384 4096 1.348 0.130 10.4x
32768 4096 1.229 0.097 12.7x
65536 4096 1.021 0.074 13.8x
32768 512 1.226 0.185 6.6x
32768 64 1.300 1.072 1.21x

A 64-entry buffer is the worst case: only one word per bulk step, so almost everything goes through the scalar loop. It is still slightly faster, not a regression.

BenchmarkRealDataNextMany over the whole corpus:

dataset master this PR
census-income 10.19 ms 3.41 ms 2.99x
weather_sept_85 20.22 ms 7.42 ms 2.72x

Testing

go test ./..., roaring64, the smat corpus and smat-hit checks, go tool unconvert, gofmt, go vet, and cross-builds for 386/arm/arm64 and -tags appengine, on AVX-512 hardware so the bulk path executes.

TestBitmapContainerNextManyBufferSizes iterates containers of cardinality 1, 63, 64, 65, 4096, 30000, 65535 and 65536 through buffers of 1, 2, 63, 64, 65, 100, 127, 128, 129, 1000, 4096 and 65536 entries, comparing the concatenated output against the expected values and checking that no call ever returns more than the buffer holds. The sizes that are not multiples of 64 are the point: they force the bulk and scalar paths to interleave mid-container.

Breaking Changes

None. NextMany's contract, ordering and buffer handling are unchanged.

https://claude.ai/code/session_0123ePBefqPjrCvhxdwWFakj

nextMany walked set bits one at a time. When the current word is exhausted
it now decodes whole words with the AVX-512 kernel for as long as the
caller's buffer has room for the 64 values a single word could produce,
falling back to the word-at-a-time loop for the partial word. bitset stays
zero across the bulk step, preserving the invariant that word `base` has
been fully consumed.

The word count per step is room/64, which cannot overflow the buffer
whatever the data looks like.

Xeon Gold 6548N, ns per value, 4096-entry buffer:

    cardinality 4096    1.240 -> 0.418  (2.97x)
    cardinality 16384   1.348 -> 0.130  (10.4x)
    cardinality 32768   1.229 -> 0.097  (12.7x)
    cardinality 65536   1.021 -> 0.074  (13.8x)

BenchmarkRealDataNextMany over the whole corpus:

    census-income     10.19 ms -> 3.41 ms  (2.99x)
    weather_sept_85   20.22 ms -> 7.42 ms  (2.72x)

A 64-entry buffer is the worst case, one word per bulk step; it is still
slightly faster (1.21x), not a regression.

nextMany64 is left alone: []uint64 output needs a byte-to-quadword widen,
a different kernel with a different trade-off.

Claude-Session: https://claude.ai/code/session_0123ePBefqPjrCvhxdwWFakj
@lemire
lemire merged commit 67fb8b9 into vbmi2-toarray Aug 27, 2026
12 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.

1 participant