Repository navigation
Speed up bitmap container NextMany on arm64 with TrailingZeros64 - #591
Merged
lemire merged 1 commit intoNov 12, 2026
Merged
Conversation
arm64 has no scalar popcount, so bits.OnesCount64 goes through the vector unit for every set bit. On arm64 only, use bits.TrailingZeros64 and, when the buffer has room, drain a whole word at a time, the approach from RoaringBitmap#553. Other targets keep the existing loop. A compile-time constant removes the new code there, and the amd64 machine code of nextMany and nextMany64 is byte-identical to master at GOAMD64=v1 and v3. On amd64 the effect of this rewrite varied with the microarchitecture and with small code-shape changes. Add a buffer-size test and a benchmark for nextMany64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
|
We will merge this. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Speeds up
bitmapContainerManyIterator.nextManyandnextMany64on arm64. All other targets are unchanged.Type of Change
Changes Made
What was changed?
bits.TrailingZeros64and clear them withx &= x - 1, and drain a whole word in a tight loop when the output buffer has room for 64 values.decodeWithTrailingZeros, which istrueonly indecodewords_arm64.go.TestBitmapContainerNextMany64BufferSizesandBenchmarkBitmapContainerNextMany64.Why was it changed?
bits.OnesCount64(t-1)compiles toFMOV/VCNT/UADDLV/FMOVfor every set bit, whereTrailingZeros64isRBIT/CLZ.Relationship to #553
The arm64 loop is the one proposed in #553, and it measures the same as #553 on all three Graviton generations below. The difference is scope: #553 changed every architecture, and I could not justify that for amd64 (see below). #553 has since been closed in favour of this PR. Its author re-ran it on Zen 3 and concluded that a separate amd64 path was not worth it.
Testing
go test ./...passes withGOAMD64=v1andGOAMD64=v3. The iterator tests also pass natively on Neoverse N1, V1 and V2 (-test.run 'NextMany|ManyIterator|Iterator').Fuzzing
Not run. The template's
go test -run='^$' -fuzz=FuzzSmatreports "no fuzz tests to fuzz", as noFuzzSmattarget is defined in the repository. The change does not touch deserialization. Coverage comes from the buffer-size tests, which cross the 64-slot drain boundary with buffers of 63, 64 and 65 entries and compare against a reference decode.Performance Impact
sec/opchange against master, go1.27.3,taskset-pinned, 10 rounds interleaved with master,benchstat(all p=0.000):BitmapContainerNextMany/card=4096/buf=4096BitmapContainerNextMany/card=16384/buf=4096BitmapContainerNextMany/card=32768/buf=4096BitmapContainerNextMany/card=65536/buf=4096BitmapContainerNextMany/card=32768/buf=512BitmapContainerNextMany/card=32768/buf=64BitmapContainerNextMany64(card=32768, buf=4096)Why amd64 is left alone
The machine code of
nextManyandnextMany64is byte-identical to master atGOAMD64=v1andv3(compared withgo tool objdump), so amd64 performance does not change.I tried to extend this to amd64 and the results did not support it. Measured on Cascade Lake, Ice Lake, Zen 3 (EPYC 7R13 and Ryzen 5600X) and Zen 4:
GOAMD64=v1was 10–30% slower on mostnextManycases on Zen 3, but up to 50% faster on Cascade Lake and on most cases on Ice Lake and Zen 4.v1, a version using the original popcount sequence through an inlined helper, with noTrailingZeros64at all, was still 12–20% slower than master on mostnextManycases on Zen 3. Atv3on Zen 3, two equivalent forms gave −12% and −40%.So the amd64 effect appears to depend on register allocation and layout as much as on the instruction chosen. I don't have a cause I can defend, and someone with a better handle on amd64 may want to revisit it. Other non-arm64 architectures were not measured and are also unchanged.
🤖 Generated with Claude Code