Repository navigation
Skip zero words without mispredicting in the AVX-512 uint16 decoder - #953
Merged
Merged
Conversation
array_container_from_bitset decodes sparse bitsets (at most 4096 values) with bitset_extract_setbits_avx512_uint16. Since #868, that kernel tests each word ("if (v != 0)") to skip zero words. When zero and nonzero words alternate, as when about 1000 values are spread out, that branch is mispredicted about once per value. Even after #952, gocroaring's FastOr (roaring_bitmap_or_many over 200 sparse bitmaps) remained 1.23x slower than with CRoaring 5.1.0, whose kernel had no such branch. Neither extreme works across inputs: decoding every word without testing it is fastest when most words are nonzero, but wastes time on very sparse bitsets or runs. So the kernel now picks by the capacity, which array_container_from_bitset sets to the cardinality: - from 2048 values (two per word on average), it decodes every word with masked stores, four words at a time without capacity checks while there is room for 256 values; - below 2048 values, it tests eight words at a time (VPTESTMQ) and visits only the nonzero ones, so that a run of zero words costs little and a group costs about one misprediction. Both stop once "capacity" values are written, instead of scanning all the words. Against master on a Xeon Gold 6548N, with microbenchmarks/bitset_to_array_bench (medians of 10 runs), the conversion is 1.02x to 22x faster at every cardinality from 16 to 4096 in the uniform, clustered and runs layouts, and roaring_bitmap_or_many is 1.01x to 1.43x faster. gocroaring's FastOr is now 3.6% faster than with CRoaring 5.1.0. Add a test that checks the decoder against the scalar loop on sparse bitsets with zero words, spread out or in runs, on both sides of 2048 values, with truncated capacities: the existing test only had nonzero words.
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.
Follow-up to #952. That PR removed the 1024-value gate in
array_container_from_bitset, but it did not fully fix the regression it was meant to fix. It said removing the gate recovered gocroaring'sFastOr. That three-way run was too noisy (±20–35%). A clean run (n=10, ±1–4%) against current master showsFastOrstill 1.23× slower than with CRoaring 5.1.0 (9.0 → 11.1 ms) on AVX-512 hardware.Cause
Since #868,
bitset_extract_setbits_avx512_uint16tests each word (if (v != 0)) to skip zero words. On the sparse input ofarray_container_from_bitset, zero and nonzero words often alternate; at about 1000 spread-out values, roughly 38% of words are zero. That branch is then mispredicted about once per value. (bitset_extract_intersection_setbits_avx512_uint16already avoids this test, for the same reason.) The 5.1.0 kernel had no branch, and inFastOr,perfshows the decoder going from 2.7 ms (5.1.0) to 4.4 ms (master).The benchmarks in #952 compared the two decoders of one version against each other, so they could not see the kernel itself getting slower.
Fix
Decoding every word without testing it (masked stores write nothing for v == 0) wins when most words are nonzero, but loses up to 2.3× to master on very sparse bitsets or runs. So the kernel now chooses by
capacity, whicharray_container_from_bitsetsets to the cardinality:VPTESTMQ) and visit only the nonzero ones. A run of zero words costs little, and a group costs about one misprediction.Both stop once
capacityvalues are written, instead of scanning all 1024 words; this is where the clustered layout gains most.I also tried a single strategy with a per-group dense/sparse switch, and a "sticky" dense mode. Both stayed at 0.83–0.85× master on uniform 4096. This split is the only version I measured that is at least as fast as master in every cell.
Results
Xeon Gold 6548N (Emerald Rapids), pinned to one core, medians of 10 runs,
microbenchmarks/bitset_to_array_bench. Each cell is ns, master → this PR:roaring_bitmap_or_many(1024 chunks, 5 values per chunk per bitmap):gocroaring, against CRoaring 5.1.0
Full suite of 61 benchmarks, n=10, AVX-512:
FastOris now 3.6% faster than with 5.1.0 (9.02 → 8.69 ms). It was 42% slower with 5.4.0 and 23% slower with master.On AVX2-only builds (
-DCROARING_COMPILER_SUPPORTS_AVX512=0; this PR changes nothing there), master is 11% faster than 5.1.0, andFastOris unchanged.A few gocroaring benchmarks were 1–2.6% slower than with 5.1.0 even though the C code on their paths is identical, or differs only by the skew check from #913 in
array_container_andnot. Rebuilt with-falign-functions=64 -falign-loops=64 -falign-jumps=64, all of them disappear into the noise butIntersecton a run and an array container: +2.8%, or 1.5 ns on a 55 ns cgo call. That path is identical C code in both versions, so I take it as a code-layout effect.Tests
New
avx512_extract_uint16_sparse_matches_scalarchecks the decoder against the scalar loop on sparse bitsets with zero words:The existing test only generates nonzero words, so it never exercised zero-word skipping. Removing the
i += jtruncation bookkeeping makes the new test fail.All 27 tests pass, and the changed files are clean under clang-format 17.