From 4cc8e21eb99710034cab53e634ff889879b285e4 Mon Sep 17 00:00:00 2001 From: Guy Molinari Date: Sat, 18 Jul 2026 17:40:49 -0600 Subject: [PATCH 1/7] Optimize roaring64 BSI BatchEqual --- roaring64/bsi64.go | 126 +++++++++++++++++- roaring64/bsi64_batch_equal_test.go | 198 ++++++++++++++++++++++++++++ 2 files changed, 317 insertions(+), 7 deletions(-) create mode 100644 roaring64/bsi64_batch_equal_test.go diff --git a/roaring64/bsi64.go b/roaring64/bsi64.go index e9b1eca4..5e4377c4 100644 --- a/roaring64/bsi64.go +++ b/roaring64/bsi64.go @@ -5,6 +5,7 @@ import ( "io" "math/big" "runtime" + "sort" "sync" ) @@ -970,25 +971,136 @@ func (b *BSI) WriteTo(w io.Writer) (n int64, err error) { // BatchEqual returns a bitmap containing the column IDs where the values are contained within the list of values provided. func (b *BSI) BatchEqual(parallelism int, values []int64) *Bitmap { - //convert list of int64 values to big.Int(s) - bigValues := make([]*big.Int, len(values)) - for i, v := range values { - bigValues[i] = big.NewInt(v) + if b.eBM.IsEmpty() || len(values) == 0 { + return NewBitmap() } - return b.BatchEqualBig(parallelism, bigValues) + + bitCount := b.BitCount() + if bitCount >= 64 { + // Fall back to the arbitrary-precision path when the BSI has more than + // int64's finite bit width. This preserves correctness for big-value BSIs. + bigValues := make([]*big.Int, len(values)) + for i, v := range values { + bigValues[i] = big.NewInt(v) + } + return b.BatchEqualBig(parallelism, bigValues) + } + + seen := make(map[uint64]struct{}, len(values)) + vals := make([]uint64, 0, len(values)) + for _, v := range values { + if !bsi64ValueFitsBitCount(v, bitCount) { + continue + } + encoded := encodeBSI64Value(v, bitCount) + if _, ok := seen[encoded]; ok { + continue + } + seen[encoded] = struct{}{} + vals = append(vals, encoded) + } + if len(vals) == 0 { + return NewBitmap() + } + + sort.Slice(vals, func(i, j int) bool { return vals[i] < vals[j] }) + result := b.matchInt64Trie(vals, bitCount, &b.eBM, false) + if b.runOptimized { + result.RunOptimize() + } + return result +} + +func bsi64ValueFitsBitCount(value int64, bitCount int) bool { + if bitCount >= 63 { + return true + } + min := -(int64(1) << uint(bitCount)) + max := (int64(1) << uint(bitCount)) - 1 + return value >= min && value <= max +} + +func encodeBSI64Value(value int64, bitCount int) uint64 { + if bitCount >= 63 { + return uint64(value) + } + mask := (uint64(1) << uint(bitCount+1)) - 1 + return uint64(value) & mask +} + +func (b *BSI) matchInt64Trie(vals []uint64, p int, prefix *Bitmap, owned bool) *Bitmap { + if prefix.IsEmpty() { + if owned { + return prefix + } + return NewBitmap() + } + if p < 0 || (p < 63 && uint64(len(vals)) == uint64(1)< Date: Sat, 18 Jul 2026 17:47:23 -0600 Subject: [PATCH 2/7] Optimize BSI64 BatchEqual cube patterns --- roaring64/bsi64.go | 56 +++++++++++++++++++++++++++++ roaring64/bsi64_batch_equal_test.go | 16 +++++++++ 2 files changed, 72 insertions(+) diff --git a/roaring64/bsi64.go b/roaring64/bsi64.go index 5e4377c4..e147e4e8 100644 --- a/roaring64/bsi64.go +++ b/roaring64/bsi64.go @@ -1004,6 +1004,12 @@ func (b *BSI) BatchEqual(parallelism int, values []int64) *Bitmap { } sort.Slice(vals, func(i, j int) bool { return vals[i] < vals[j] }) + if result, ok := b.matchInt64Cube(vals, bitCount); ok { + if b.runOptimized { + result.RunOptimize() + } + return result + } result := b.matchInt64Trie(vals, bitCount, &b.eBM, false) if b.runOptimized { result.RunOptimize() @@ -1028,6 +1034,56 @@ func encodeBSI64Value(value int64, bitCount int) uint64 { return uint64(value) & mask } +func (b *BSI) matchInt64Cube(vals []uint64, bitCount int) (*Bitmap, bool) { + if bitCount >= 63 { + return nil, false + } + widthMask := (uint64(1) << uint(bitCount+1)) - 1 + fixedOnes := vals[0] & widthMask + fixedZeros := ^vals[0] & widthMask + for _, v := range vals[1:] { + fixedOnes &= v + fixedZeros &= ^v & widthMask + } + + variableMask := ^(fixedOnes | fixedZeros) & widthMask + combinations := uint64(1) << uint(countBSI64Bits(variableMask)) + if uint64(len(vals)) != combinations { + return nil, false + } + for _, v := range vals { + if v&fixedOnes != fixedOnes || (^v)&fixedZeros != fixedZeros { + return nil, false + } + } + + result := b.eBM.Clone() + for i := 0; i <= bitCount; i++ { + bit := uint64(1) << uint(i) + if variableMask&bit != 0 { + continue + } + if fixedOnes&bit != 0 { + result.And(&b.bA[i]) + } else { + result.AndNot(&b.bA[i]) + } + if result.IsEmpty() { + break + } + } + return result, true +} + +func countBSI64Bits(value uint64) int { + count := 0 + for value != 0 { + value &= value - 1 + count++ + } + return count +} + func (b *BSI) matchInt64Trie(vals []uint64, p int, prefix *Bitmap, owned bool) *Bitmap { if prefix.IsEmpty() { if owned { diff --git a/roaring64/bsi64_batch_equal_test.go b/roaring64/bsi64_batch_equal_test.go index fe7daa63..475d689e 100644 --- a/roaring64/bsi64_batch_equal_test.go +++ b/roaring64/bsi64_batch_equal_test.go @@ -135,6 +135,22 @@ func TestBSI64BatchEqualConsistentWithGetValue(t *testing.T) { } } +func TestBSI64BatchEqualBitCubePattern(t *testing.T) { + bsi := NewDefaultBSI() + for col := uint64(0); col < 512; col++ { + bsi.SetValue(col, int64(col%256)) + } + + odds := make([]int64, 0, 128) + for v := int64(1); v < 256; v += 2 { + odds = append(odds, v) + } + + expected := expectedBSI64BatchEqual(bsi, odds) + actual := bsi.BatchEqual(0, odds) + assert.True(t, actual.Equals(expected), "expected %v got %v", expected.ToArray(), actual.ToArray()) +} + func TestBSI64BatchEqualExistenceAuthority(t *testing.T) { ebm := BitmapOf(1) plane := BitmapOf(1, 2) From f01d23ccf5747f9e375b8ed3851482ea99a07031 Mon Sep 17 00:00:00 2001 From: Guy Molinari Date: Sat, 18 Jul 2026 17:55:13 -0600 Subject: [PATCH 3/7] Optimize BSI64 int64 comparisons --- roaring64/bsi64.go | 87 +++++++++++++ roaring64/bsi64_compare_benchmark_test.go | 145 ++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 roaring64/bsi64_compare_benchmark_test.go diff --git a/roaring64/bsi64.go b/roaring64/bsi64.go index e147e4e8..d2dc50f4 100644 --- a/roaring64/bsi64.go +++ b/roaring64/bsi64.go @@ -349,9 +349,96 @@ type task struct { func (b *BSI) CompareValue(parallelism int, op Operation, valueOrStart, end int64, foundSet *Bitmap) *Bitmap { + if op == EQ { + result := b.BatchEqual(parallelism, []int64{valueOrStart}) + if foundSet != nil { + result.And(foundSet) + } + return result + } + if result, ok := b.compareInt64Value(op, valueOrStart, end, foundSet); ok { + return result + } return b.CompareBigValue(parallelism, op, big.NewInt(valueOrStart), big.NewInt(end), foundSet) } +func (b *BSI) compareInt64Value(op Operation, valueOrStart, end int64, foundSet *Bitmap) (*Bitmap, bool) { + bitCount := b.BitCount() + if bitCount > 63 || !bsi64ValueFitsBitCount(valueOrStart, bitCount) { + return nil, false + } + if op == RANGE && !bsi64ValueFitsBitCount(end, bitCount) { + return nil, false + } + + universe := b.eBM.Clone() + if foundSet != nil { + universe.And(foundSet) + } + if universe.IsEmpty() { + return universe, true + } + + start := transformBSI64SignedEncoding(encodeBSI64Value(valueOrStart, bitCount), bitCount) + less, equal := b.compareInt64LessAndEqual(start, universe) + + switch op { + case LT: + return less, true + case LE: + less.Or(equal) + return less, true + case GE: + universe.AndNot(less) + return universe, true + case GT: + less.Or(equal) + universe.AndNot(less) + return universe, true + case RANGE: + if valueOrStart > end { + return NewBitmap(), true + } + universe.AndNot(less) + finish := transformBSI64SignedEncoding(encodeBSI64Value(end, bitCount), bitCount) + rangeLess, rangeEqual := b.compareInt64LessAndEqual(finish, universe) + rangeLess.Or(rangeEqual) + return rangeLess, true + default: + return nil, false + } +} + +func transformBSI64SignedEncoding(encoded uint64, bitCount int) uint64 { + return encoded ^ (uint64(1) << uint(bitCount)) +} + +func (b *BSI) compareInt64LessAndEqual(target uint64, universe *Bitmap) (*Bitmap, *Bitmap) { + less := NewBitmap() + equalPrefix := universe.Clone() + for i := b.BitCount(); i >= 0; i-- { + targetBitSet := target&(uint64(1)<= valueOrStart { + expected.Add(col) + } + case GT: + if val > valueOrStart { + expected.Add(col) + } + case RANGE: + if val >= valueOrStart && val <= end { + expected.Add(col) + } + default: + panic("unsupported test operation") + } + } + return expected +} + +func TestBSI64CompareValueConsistentWithGetValue(t *testing.T) { + rg := rand.New(rand.NewSource(84)) + for run := 0; run < 15; run++ { + bsi := NewDefaultBSI() + numCols := rg.Intn(1000) + 10 + for col := 0; col < numCols; col++ { + if rg.Float64() < 0.8 { + bsi.SetValue(uint64(col), rg.Int63n(500)-250) + } + } + + foundSet := NewBitmap() + iter := bsi.GetExistenceBitmap().Iterator() + for iter.HasNext() { + col := iter.Next() + if col%3 != 0 { + foundSet.Add(col) + } + } + + cases := []struct { + op Operation + start int64 + end int64 + }{ + {LT, -17, 0}, + {LE, -17, 0}, + {EQ, -17, 0}, + {GE, -17, 0}, + {GT, -17, 0}, + {RANGE, -25, 25}, + } + for _, tc := range cases { + for _, fs := range []*Bitmap{nil, foundSet} { + expected := expectedBSI64CompareValue(bsi, tc.op, tc.start, tc.end, fs) + actual := bsi.CompareValue(0, tc.op, tc.start, tc.end, fs) + assert.True(t, actual.Equals(expected), "run=%d op=%d foundSet=%v expected=%v actual=%v", + run, tc.op, fs != nil, expected.ToArray(), actual.ToArray()) + } + } + } +} + +func BenchmarkBSI64CompareValueEQLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareValue(0, EQ, 55, 0, nil) + _ = res + } +} + +func BenchmarkBSI64CompareValueEQFoundSetLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + foundSet := bsi.CompareValue(0, RANGE, 40, 70, nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareValue(0, EQ, 55, 0, foundSet) + _ = res + } +} + +func BenchmarkBSI64CompareValueRangeLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareValue(0, RANGE, 40, 70, nil) + _ = res + } +} + +func BenchmarkBSI64CompareValueGELargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareValue(0, GE, 55, 0, nil) + _ = res + } +} From f4194573e364c39ef6a6bb3ee525bd8977c5d0fc Mon Sep 17 00:00:00 2001 From: Guy Molinari Date: Sat, 18 Jul 2026 18:03:57 -0600 Subject: [PATCH 4/7] Optimize BSI64 int64 comparisons --- roaring64/bsi64.go | 60 +++++++++++++++++++---- roaring64/bsi64_batch_equal_test.go | 14 ++++++ roaring64/bsi64_compare_benchmark_test.go | 55 +++++++++++++++++++++ 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/roaring64/bsi64.go b/roaring64/bsi64.go index d2dc50f4..5cf2c5e2 100644 --- a/roaring64/bsi64.go +++ b/roaring64/bsi64.go @@ -349,24 +349,24 @@ type task struct { func (b *BSI) CompareValue(parallelism int, op Operation, valueOrStart, end int64, foundSet *Bitmap) *Bitmap { - if op == EQ { - result := b.BatchEqual(parallelism, []int64{valueOrStart}) - if foundSet != nil { - result.And(foundSet) - } - return result - } - if result, ok := b.compareInt64Value(op, valueOrStart, end, foundSet); ok { + if result, ok := b.compareInt64Value(parallelism, op, valueOrStart, end, foundSet); ok { return result } return b.CompareBigValue(parallelism, op, big.NewInt(valueOrStart), big.NewInt(end), foundSet) } -func (b *BSI) compareInt64Value(op Operation, valueOrStart, end int64, foundSet *Bitmap) (*Bitmap, bool) { +func (b *BSI) compareInt64Value(parallelism int, op Operation, valueOrStart, end int64, foundSet *Bitmap) (*Bitmap, bool) { bitCount := b.BitCount() if bitCount > 63 || !bsi64ValueFitsBitCount(valueOrStart, bitCount) { return nil, false } + if op == EQ { + result := b.BatchEqual(parallelism, []int64{valueOrStart}) + if foundSet != nil { + result.And(foundSet) + } + return result, true + } if op == RANGE && !bsi64ValueFitsBitCount(end, bitCount) { return nil, false } @@ -456,6 +456,10 @@ func (b *BSI) CompareBigValue(parallelism int, op Operation, valueOrStart, end * end = b.MinMaxBig(parallelism, MAX, &b.eBM) } + if result, ok := b.compareBigValueAsInt64(parallelism, op, valueOrStart, end, foundSet); ok { + return result + } + comp := &task{bsi: b, op: op, valueOrStart: valueOrStart, end: end} if foundSet == nil { return parallelExecutor(parallelism, comp, compareValue, &b.eBM) @@ -463,6 +467,20 @@ func (b *BSI) CompareBigValue(parallelism int, op Operation, valueOrStart, end * return parallelExecutor(parallelism, comp, compareValue, foundSet) } +func (b *BSI) compareBigValueAsInt64(parallelism int, op Operation, valueOrStart, end *big.Int, foundSet *Bitmap) (*Bitmap, bool) { + if valueOrStart == nil || !valueOrStart.IsInt64() { + return nil, false + } + endValue := int64(0) + if op == RANGE { + if end == nil || !end.IsInt64() { + return nil, false + } + endValue = end.Int64() + } + return b.compareInt64Value(parallelism, op, valueOrStart.Int64(), endValue, foundSet) +} + // Returns a twos complement value given a value, the return will be bit extended to 'bits' length // if the value is negative func twosComplement(num *big.Int, bitCount int) *big.Int { @@ -1222,6 +1240,10 @@ func (b *BSI) BatchEqualBig(parallelism int, values []*big.Int) *Bitmap { return NewBitmap() } + if intValues, ok := b.batchEqualBigValuesAsInt64(values); ok { + return b.BatchEqual(parallelism, intValues) + } + valMap := make(map[string]struct{}, len(values)) for i := 0; i < len(values); i++ { if values[i] == nil { @@ -1244,6 +1266,26 @@ func batchEqualBigKey(value *big.Int) string { return string(key) } +func (b *BSI) batchEqualBigValuesAsInt64(values []*big.Int) ([]int64, bool) { + if b.BitCount() > 63 { + return nil, false + } + intValues := make([]int64, 0, len(values)) + for _, value := range values { + if value == nil { + continue + } + if !value.IsInt64() { + return nil, false + } + intValues = append(intValues, value.Int64()) + } + if len(intValues) == 0 { + return nil, false + } + return intValues, true +} + func batchEqual(e *task, batch []uint64, resultsChan chan *Bitmap, wg *sync.WaitGroup) { diff --git a/roaring64/bsi64_batch_equal_test.go b/roaring64/bsi64_batch_equal_test.go index 475d689e..73716daa 100644 --- a/roaring64/bsi64_batch_equal_test.go +++ b/roaring64/bsi64_batch_equal_test.go @@ -1,6 +1,7 @@ package roaring64 import ( + "math/big" "math/rand" "testing" @@ -193,6 +194,19 @@ func BenchmarkBSI64BatchEqualLargeAgeFixture(b *testing.B) { } } +func BenchmarkBSI64BatchEqualBigLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + values := []*big.Int{big.NewInt(55), big.NewInt(57)} + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.BatchEqualBig(0, values) + _ = res + } +} + func BenchmarkBSI64BatchEqualM128(b *testing.B) { benchmarkBSI64BatchEqualM(b, 128, 1) } func BenchmarkBSI64BatchEqualM128Scattered(b *testing.B) { benchmarkBSI64BatchEqualM(b, 128, 2) } func BenchmarkBSI64BatchEqualM200(b *testing.B) { benchmarkBSI64BatchEqualM(b, 200, 1) } diff --git a/roaring64/bsi64_compare_benchmark_test.go b/roaring64/bsi64_compare_benchmark_test.go index ccde7258..dd151200 100644 --- a/roaring64/bsi64_compare_benchmark_test.go +++ b/roaring64/bsi64_compare_benchmark_test.go @@ -1,6 +1,7 @@ package roaring64 import ( + "math/big" "math/rand" "testing" @@ -107,6 +108,19 @@ func BenchmarkBSI64CompareValueEQLargeAgeFixture(b *testing.B) { } } +func BenchmarkBSI64CompareBigValueEQLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + value := big.NewInt(55) + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareBigValue(0, EQ, value, nil, nil) + _ = res + } +} + func BenchmarkBSI64CompareValueEQFoundSetLargeAgeFixture(b *testing.B) { bsi := setupLargeBSI(b) if bsi == nil { @@ -120,6 +134,20 @@ func BenchmarkBSI64CompareValueEQFoundSetLargeAgeFixture(b *testing.B) { } } +func BenchmarkBSI64CompareBigValueEQFoundSetLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + foundSet := bsi.CompareBigValue(0, RANGE, big.NewInt(40), big.NewInt(70), nil) + value := big.NewInt(55) + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareBigValue(0, EQ, value, nil, foundSet) + _ = res + } +} + func BenchmarkBSI64CompareValueRangeLargeAgeFixture(b *testing.B) { bsi := setupLargeBSI(b) if bsi == nil { @@ -132,6 +160,20 @@ func BenchmarkBSI64CompareValueRangeLargeAgeFixture(b *testing.B) { } } +func BenchmarkBSI64CompareBigValueRangeLargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + start := big.NewInt(40) + end := big.NewInt(70) + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareBigValue(0, RANGE, start, end, nil) + _ = res + } +} + func BenchmarkBSI64CompareValueGELargeAgeFixture(b *testing.B) { bsi := setupLargeBSI(b) if bsi == nil { @@ -143,3 +185,16 @@ func BenchmarkBSI64CompareValueGELargeAgeFixture(b *testing.B) { _ = res } } + +func BenchmarkBSI64CompareBigValueGELargeAgeFixture(b *testing.B) { + bsi := setupLargeBSI(b) + if bsi == nil { + b.Skip("skipping, large BSI setup failed") + } + value := big.NewInt(55) + b.ResetTimer() + for i := 0; i < b.N; i++ { + res := bsi.CompareBigValue(0, GE, value, nil, nil) + _ = res + } +} From 8212ae89abd3c0e5817debae3ccdf69fd8ef84e6 Mon Sep 17 00:00:00 2001 From: Guy Molinari Date: Sat, 18 Jul 2026 18:09:12 -0600 Subject: [PATCH 5/7] Document BSI64 performance coverage --- roaring64/BSI_BENCHMARKS.md | 38 ++++++++ roaring64/bsi64_compare_benchmark_test.go | 104 ++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 roaring64/BSI_BENCHMARKS.md diff --git a/roaring64/BSI_BENCHMARKS.md b/roaring64/BSI_BENCHMARKS.md new file mode 100644 index 00000000..5c5e21ee --- /dev/null +++ b/roaring64/BSI_BENCHMARKS.md @@ -0,0 +1,38 @@ +# BSI64 Benchmarks + +These notes capture local benchmark results for the BSI64 `BatchEqual` and +comparison paths. They are intended as reproducible PR evidence, not as +contractual performance guarantees. + +Environment: + +- CPU: 12th Gen Intel(R) Core(TM) i7-1255U +- OS/arch: linux/amd64 +- Package: `github.com/RoaringBitmap/roaring/v2/roaring64` + +Commands: + +```sh +go test ./roaring64 -count=1 +go test ./roaring64 -run '^$' -bench 'BenchmarkBSI64BatchEqual' -benchmem -count 3 +go test ./roaring64 -run '^$' -bench 'BenchmarkBSI64Compare(Big)?Value|BenchmarkBSI64BatchEqual(Big)?LargeAgeFixture' -benchmem -count 1 +``` + +Representative results: + +| Benchmark | Before | After | Notes | +| --- | ---: | ---: | --- | +| `BenchmarkBSI64BatchEqualLargeAgeFixture` | ~13-14s/op, ~12.4GB/op | ~145-205ms/op, ~25.5MB/op | Avoids row-by-row `GetBigValue` for int64-width values. | +| `BenchmarkBSI64BatchEqualM128Scattered` | ~1.25s/op, ~458MB/op | ~11-17ms/op, ~12.5MB/op | Detects complete bit-cube value patterns. | +| `BenchmarkBSI64CompareValueEQLargeAgeFixture` | ~4.44s/op, ~461MB/op | ~100-118ms/op, ~19.7MB/op | `EQ` delegates to optimized `BatchEqual`. | +| `BenchmarkBSI64CompareValueRangeLargeAgeFixture` | ~7.49s/op, ~501MB/op | ~204-224ms/op, ~122.6MB/op | Uses bitmap-native signed int64 comparison. | +| `BenchmarkBSI64CompareValueGELargeAgeFixture` | ~3.45s/op, ~500MB/op | ~168-184ms/op, ~82.3MB/op | Uses bitmap-native signed int64 comparison. | + +Compatibility: + +- Public method signatures are unchanged. +- `CompareBigValue` and `BatchEqualBig` internally delegate to the optimized + int64 paths only when the BSI and query values fit in signed 64-bit space. +- True wider-than-64-bit values continue to use the existing generic paths. +- `BatchEqualBig` now keys values by sign and magnitude so positive and negative + values with the same magnitude do not collide. diff --git a/roaring64/bsi64_compare_benchmark_test.go b/roaring64/bsi64_compare_benchmark_test.go index dd151200..7fa0ce47 100644 --- a/roaring64/bsi64_compare_benchmark_test.go +++ b/roaring64/bsi64_compare_benchmark_test.go @@ -96,6 +96,110 @@ func TestBSI64CompareValueConsistentWithGetValue(t *testing.T) { } } +func TestBSI64CompareBigValueConsistentWithGetBigValue(t *testing.T) { + rg := rand.New(rand.NewSource(85)) + for run := 0; run < 15; run++ { + bsi := NewDefaultBSI() + numCols := rg.Intn(1000) + 10 + for col := 0; col < numCols; col++ { + if rg.Float64() < 0.8 { + bsi.SetValue(uint64(col), rg.Int63n(500)-250) + } + } + + foundSet := NewBitmap() + iter := bsi.GetExistenceBitmap().Iterator() + for iter.HasNext() { + col := iter.Next() + if col%3 != 0 { + foundSet.Add(col) + } + } + + cases := []struct { + op Operation + start int64 + end int64 + }{ + {LT, -17, 0}, + {LE, -17, 0}, + {EQ, -17, 0}, + {GE, -17, 0}, + {GT, -17, 0}, + {RANGE, -25, 25}, + } + for _, tc := range cases { + for _, fs := range []*Bitmap{nil, foundSet} { + expected := expectedBSI64CompareBigValue(bsi, tc.op, big.NewInt(tc.start), big.NewInt(tc.end), fs) + actual := bsi.CompareBigValue(0, tc.op, big.NewInt(tc.start), big.NewInt(tc.end), fs) + assert.True(t, actual.Equals(expected), "run=%d op=%d foundSet=%v expected=%v actual=%v", + run, tc.op, fs != nil, expected.ToArray(), actual.ToArray()) + } + } + } +} + +func expectedBSI64CompareBigValue(bsi *BSI, op Operation, valueOrStart, end *big.Int, foundSet *Bitmap) *Bitmap { + expected := NewBitmap() + source := bsi.GetExistenceBitmap() + if foundSet != nil { + source = And(source, foundSet) + } + iter := source.Iterator() + for iter.HasNext() { + col := iter.Next() + val, ok := bsi.GetBigValue(col) + if !ok { + continue + } + switch op { + case LT: + if val.Cmp(valueOrStart) < 0 { + expected.Add(col) + } + case LE: + if val.Cmp(valueOrStart) <= 0 { + expected.Add(col) + } + case EQ: + if val.Cmp(valueOrStart) == 0 { + expected.Add(col) + } + case GE: + if val.Cmp(valueOrStart) >= 0 { + expected.Add(col) + } + case GT: + if val.Cmp(valueOrStart) > 0 { + expected.Add(col) + } + case RANGE: + if val.Cmp(valueOrStart) >= 0 && val.Cmp(end) <= 0 { + expected.Add(col) + } + default: + panic("unsupported test operation") + } + } + return expected +} + +func TestBSI64CompareBigValueFallsBackForBigWidth(t *testing.T) { + bsi := NewDefaultBSI() + base := new(big.Int).Lsh(big.NewInt(1), 80) + below := new(big.Int).Sub(base, big.NewInt(1)) + above := new(big.Int).Add(base, big.NewInt(1)) + bsi.SetBigValue(1, below) + bsi.SetBigValue(2, base) + bsi.SetBigValue(3, above) + + eq := bsi.CompareBigValue(0, EQ, base, nil, nil) + assert.True(t, eq.Equals(BitmapOf(2))) + + rng := bsi.CompareBigValue(0, RANGE, base, above, nil) + assert.True(t, rng.Equals(BitmapOf(2, 3))) +} + func BenchmarkBSI64CompareValueEQLargeAgeFixture(b *testing.B) { bsi := setupLargeBSI(b) if bsi == nil { From ce6964fafb110bfa0eeafd14f538fe9b7a6085cd Mon Sep 17 00:00:00 2001 From: Guy Molinari Date: Sat, 18 Jul 2026 18:17:07 -0600 Subject: [PATCH 6/7] Clarify BSI64 benchmark scope --- roaring64/BSI_BENCHMARKS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/roaring64/BSI_BENCHMARKS.md b/roaring64/BSI_BENCHMARKS.md index 5c5e21ee..506a37f2 100644 --- a/roaring64/BSI_BENCHMARKS.md +++ b/roaring64/BSI_BENCHMARKS.md @@ -36,3 +36,9 @@ Compatibility: - True wider-than-64-bit values continue to use the existing generic paths. - `BatchEqualBig` now keys values by sign and magnitude so positive and negative values with the same magnitude do not collide. + +Follow-up: + +- This change is scoped to `roaring64`. The 32-bit `BitSliceIndexing` package + already has separate `BatchEqual` coverage, and `CompareValue` parity can be + addressed in a follow-up PR with its own benchmarks and signed-value tests. From 132f8dfeac15acc78f5a48eac98d9e56d52009b5 Mon Sep 17 00:00:00 2001 From: Guy Molinari Date: Sat, 18 Jul 2026 18:22:52 -0600 Subject: [PATCH 7/7] Clean roaring64 golint comments --- roaring64/bsi64.go | 1 + roaring64/roaring64.go | 1 + roaring64/roaringarray64.go | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/roaring64/bsi64.go b/roaring64/bsi64.go index 5cf2c5e2..a5428aef 100644 --- a/roaring64/bsi64.go +++ b/roaring64/bsi64.go @@ -128,6 +128,7 @@ func (b *BSI) SetBigValue(columnID uint64, value *big.Int) { b.eBM.Add(columnID) } +// SetBigMany sets value for all columns in foundSet. func (b *BSI) SetBigMany(foundSet *Bitmap, value *big.Int) { // If max/min values are set to zero then automatically determine bit array size if b.MaxValue == 0 && b.MinValue == 0 { diff --git a/roaring64/roaring64.go b/roaring64/roaring64.go index adb508fe..143209de 100644 --- a/roaring64/roaring64.go +++ b/roaring64/roaring64.go @@ -1241,6 +1241,7 @@ func (rb *Bitmap) GetSerializedSizeInBytes() uint64 { return rb.highlowcontainer.serializedSizeInBytes() } +// Validate checks whether the bitmap's internal containers are coherent. func (rb *Bitmap) Validate() error { return rb.highlowcontainer.validate() } diff --git a/roaring64/roaringarray64.go b/roaring64/roaringarray64.go index 09c366ff..f974f27d 100644 --- a/roaring64/roaringarray64.go +++ b/roaring64/roaringarray64.go @@ -14,7 +14,9 @@ type roaringArray64 struct { } var ( - ErrKeySortOrder = errors.New("keys were out of order") + // ErrKeySortOrder reports that container keys are out of order. + ErrKeySortOrder = errors.New("keys were out of order") + // ErrCardinalityConstraint reports inconsistent array cardinality metadata. ErrCardinalityConstraint = errors.New("size of arrays was not coherent") )