BitSliceIndexing: find min and max by bit plane, not by row - #570
Merged
Merged
Conversation
BSI.MinMax divided the found set into one batch per core and had each goroutine rebuild every column's value bit by bit, carrying a running minimum or maximum as it went. That running comparison mishandles values of mixed sign. With two columns holding -45 and 1, and no concurrency at all, it answers 0, a value that was never stored. Which answer came back depended on how many batches the work was divided into, so the same data gave different results on machines with different core counts. TestMinMaxWithNil catches it only when its random values land on a failing pattern, roughly once in two hundred runs, which is why this has read as a flaky test rather than a wrong one. MinMax now narrows the candidate columns one bit plane at a time, highest plane first, keeping only those that can still hold the answer, then decodes the single column that survives. Below the sign, two's complement orders the same way as unsigned, so the maximum keeps the columns whose bit is set and the minimum keeps those whose bit is clear. The sign plane, which exists only in a 64 bit wide BSI, is settled first. This is the approach roaring64.MinMaxBig has used since 8a20f50. The parallelism argument is kept for compatibility and ignored, as it is there, and minOrMax, which had no other caller, is removed. MinMax also intersects the found set with the existence bitmap now, so a column carrying no value can no longer contribute a zero to the result. BenchmarkMinMax (added), Apple M1, before and after: non-negative, 100 rows 9.2 µs 1456 B 23 allocs -> 1.1 µs 1368 B 36 allocs non-negative, 10000 rows 273 µs 41940 B 23 allocs -> 11 µs 28312 B 36 allocs signed, 100 rows 52.2 µs 1456 B 23 allocs -> 8.1 µs 13856 B 207 allocs signed, 10000 rows 878 µs 41938 B 23 allocs -> 60 µs 31032 B 150 allocs A hundred rows across 64 planes is the one shape that costs more memory, since every plane intersection builds a bitmap however few rows remain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
I'm fine with this Daniel when you are ready to merge |
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
BSI.MinMaxreturns wrong answers for values of mixed sign. With two columns holding-45and1, and no concurrency at all, it answers0, a value that was never stored.This replaces the row-by-row search with the bit plane descent that
roaring64.MinMaxBigalready uses, and adds a regression test.Type of Change
Changes Made
What was changed?
MinMaxnow narrows the candidate columns one bit plane at a time, highest plane first, and decodes only the single column that survives.minOrMax, which had no other caller, is removed.MinMaxintersects the found set with the existence bitmap, so a column carrying no value can no longer contribute a zero.TestMinMaxMixedSignandBenchmarkMinMax.Why was it changed?
TestMinMaxWithNilalready catches this, but only when its clock-seeded random values land on a failing pattern, roughly once in two hundred runs. That reads as a flaky test rather than a wrong answer, which is how it has survived.How was it changed?
parallelismargument is kept for compatibility and ignored, matchingroaring64.MinMaxBig.Testing
TestMinMaxMixedSignis added and fails on master withexpected: 1, actual: 0.Checked against a brute force scan over 20000 random cases covering three bit widths, both signs, cardinalities from 1 to 40, found sets that are nil or a random subset, found sets containing a column with no value, and three values of
parallelism.Formatting
gofmt -s -l .reports nothing.go vet ./...reports only the pre-existingReadFromsignature complaint on master.Fuzzing
The step above cannot be run as written: there is no
FuzzSmattarget on master.smat.gorefers to asmat_fuzz_test.gothat is not in the repository, sogo test -fuzz=FuzzSmatmatches nothing and simply runs the ordinary tests. What does run is the smat corpus, which passes:Performance Impact
BenchmarkMinMaxis added. Apple M1, median of three.Before:
After:
Performance Analysis
Breaking Changes
No signature changes. Two behaviours differ:
parallelismis now ignored. Callers passing a value get the same answer as before, only correct, and there is nothing left to divide.Min64BitSignedforMAXandMax64BitSignedforMIN, as before.Related Issues
None filed. I could not find an issue or pull request covering this.
Additional Notes
roaring64fixed the same defect in passing. #544 rewroteMinMaxBigto prune by sign and value bit planes, and its checklist marks it a performance and refactoring change with the bug fix box unchecked, so the correctness repair went unrecorded. I confirmed the 64 bit path is now correct under the same fixture that breaks this one, with zero disagreements in 20000 trials. Its supersededminOrMaxis still present and unreachable atroaring64/bsi64.go, which I have left alone as out of scope here.🤖 Generated with Claude Code