Skip to content

Commit 292bf84

Browse files
RaduBerindelemire
authored andcommitted
Improve binary fuse parameter testing
We add a test that shows the range of sizes and segment counts for each segment length. We also add a test that checks filter generation at "boundary" sizes in terms of segment lengths. The test prints the average and max number of iterations for each tested size. Output with numTrials=100: ``` size: 2 iterations: 1.02 avg (2 max) size: 8 iterations: 1.02 avg (2 max) size: 24 iterations: 1.13 avg (3 max) size: 27 iterations: 1.02 avg (2 max) size: 55 iterations: 1.02 avg (2 max) size: 91 iterations: 1.04 avg (3 max) size: 120 iterations: 1.00 avg (1 max) size: 303 iterations: 1.09 avg (3 max) size: 349 iterations: 1.04 avg (2 max) size: 1009 iterations: 1.02 avg (2 max) size: 1124 iterations: 1.13 avg (2 max) size: 3361 iterations: 1.03 avg (3 max) size: 3551 iterations: 9.45 avg (42 max) size: 11192 iterations: 1.03 avg (2 max) size: 11521 iterations: 109.79 avg (528 max) size: 37272 iterations: 1.00 avg (1 max) size: 37454 iterations: 15.42 avg (70 max) size: 124117 iterations: 1.02 avg (2 max) size: 126131 iterations: 1.70 avg (6 max) size: 413309 iterations: 1.01 avg (2 max) size: 416077 iterations: 1.83 avg (6 max) size: 1376321 iterations: 1.00 avg (1 max) ```
1 parent dc287a3 commit 292bf84

2 files changed

Lines changed: 140 additions & 4 deletions

File tree

binaryfusefilter.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ type BinaryFuseBuilder struct {
5656
//
5757
// The function may return an error if the set is empty.
5858
func BuildBinaryFuse[T Unsigned](b *BinaryFuseBuilder, keys []uint64) (BinaryFuse[T], error) {
59+
f, _, err := buildBinaryFuse[T](b, keys)
60+
return f, err
61+
}
62+
63+
func buildBinaryFuse[T Unsigned](b *BinaryFuseBuilder, keys []uint64) (_ BinaryFuse[T], iterations int, _ error) {
5964
size := uint32(len(keys))
6065
var filter BinaryFuse[T]
6166
filter.initializeParameters(b, size)
@@ -78,13 +83,12 @@ func BuildBinaryFuse[T Unsigned](b *BinaryFuseBuilder, keys []uint64) (BinaryFus
7883
var h012 [6]uint32
7984
// this could be used to compute the mod3
8085
// tabmod3 := [5]uint8{0,1,2,0,1}
81-
iterations := 0
8286
for {
8387
iterations += 1
8488
if iterations > MaxIterations {
8589
// The probability of this happening is lower than the cosmic-ray
8690
// probability (i.e., a cosmic ray corrupts your system).
87-
return BinaryFuse[T]{}, errors.New("too many iterations")
91+
return BinaryFuse[T]{}, iterations, errors.New("too many iterations")
8892
}
8993

9094
blockBits := 1
@@ -228,7 +232,7 @@ func BuildBinaryFuse[T Unsigned](b *BinaryFuseBuilder, keys []uint64) (BinaryFus
228232
filter.Seed = splitmix64(&rngcounter)
229233
}
230234
if size == 0 {
231-
return filter, nil
235+
return filter, iterations, nil
232236
}
233237

234238
for i := int(size - 1); i >= 0; i-- {
@@ -245,7 +249,7 @@ func BuildBinaryFuse[T Unsigned](b *BinaryFuseBuilder, keys []uint64) (BinaryFus
245249
filter.Fingerprints[h012[found]] = xor2 ^ filter.Fingerprints[h012[found+1]] ^ filter.Fingerprints[h012[found+2]]
246250
}
247251

248-
return filter, nil
252+
return filter, iterations, nil
249253
}
250254

251255
func (filter *BinaryFuse[T]) initializeParameters(b *BinaryFuseBuilder, size uint32) {

binaryfusefilter_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"fmt"
55
"math/rand/v2"
66
"slices"
7+
"sort"
8+
"strings"
9+
"sync"
710
"testing"
811

912
"github.com/cespare/xxhash/v2"
@@ -377,3 +380,132 @@ func crossCheckFuseBuilder[T Unsigned](t *testing.T, bld *BinaryFuseBuilder, key
377380
_ = expected
378381
require.Equal(t, *expected, filter)
379382
}
383+
384+
// segmentLengthSizes contains represents the range of sizes [startSize, endSize] that
385+
// all get the same segmentLength.
386+
type segmentLengthSizes struct {
387+
segmentLength uint32
388+
startSize uint32
389+
startSegmentCount uint32
390+
endSize uint32
391+
endSegmentCount uint32
392+
}
393+
394+
var binaryFuseParamStableOnce struct {
395+
once sync.Once
396+
result []segmentLengthSizes
397+
}
398+
399+
const binaryFuseParamTableMaxSegmentSize = 16384
400+
401+
func binaryFuseSegLenAndCnt(size uint32) (segLen uint32, segCnt uint32) {
402+
var f BinaryFuse[uint8]
403+
f.initializeParameters(&BinaryFuseBuilder{}, size)
404+
return f.SegmentLength, f.SegmentCount
405+
}
406+
407+
func binaryFuseParamsTable() []segmentLengthSizes {
408+
binaryFuseParamStableOnce.once.Do(func() {
409+
var table []segmentLengthSizes
410+
size := uint32(1)
411+
for {
412+
segLen, segCnt := binaryFuseSegLenAndCnt(size)
413+
if segLen > binaryFuseParamTableMaxSegmentSize {
414+
break
415+
}
416+
// Find the first size that changes the segment length.
417+
n := uint32(sort.Search(int(size*4), func(x int) bool {
418+
l, _ := binaryFuseSegLenAndCnt(size + uint32(x))
419+
return l != segLen
420+
}))
421+
_, endSegCnt := binaryFuseSegLenAndCnt(size + n - 1)
422+
table = append(table, segmentLengthSizes{
423+
segmentLength: segLen,
424+
startSize: size,
425+
startSegmentCount: segCnt,
426+
endSize: size + n - 1,
427+
endSegmentCount: endSegCnt,
428+
})
429+
size += n
430+
}
431+
binaryFuseParamStableOnce.result = table
432+
})
433+
return binaryFuseParamStableOnce.result
434+
}
435+
436+
// TestBinaryFuseParams shows the segment count and size range for each segment
437+
// length. Used to verify any changes in parameter calculation.
438+
func TestBinaryFuseParams(t *testing.T) {
439+
expected := `
440+
| SegLen | SegCnt range | Size range |
441+
|--------|--------------|-------------------|
442+
| 4 | 1 - 1 | 1 - 2 |
443+
| 8 | 1 - 1 | 3 - 8 |
444+
| 16 | 1 - 2 | 9 - 27 |
445+
| 32 | 1 - 3 | 28 - 91 |
446+
| 64 | 1 - 5 | 92 - 303 |
447+
| 128 | 2 - 9 | 304 - 1009 |
448+
| 256 | 4 - 16 | 1010 - 3361 |
449+
| 512 | 7 - 26 | 3362 - 11192 |
450+
| 1024 | 12 - 42 | 11193 - 37272 |
451+
| 2048 | 20 - 69 | 37273 - 124117 |
452+
| 4096 | 34 - 114 | 124118 - 413309 |
453+
| 8192 | 56 - 188 | 413310 - 1376321 |
454+
| 16384 | 93 - 313 | 1376322 - 4583149 |
455+
`
456+
457+
var out strings.Builder
458+
fmt.Fprintf(&out, "| SegLen | SegCnt range | Size range |\n")
459+
fmt.Fprintf(&out, "|--------|--------------|-------------------|\n")
460+
for _, row := range binaryFuseParamsTable() {
461+
fmt.Fprintf(&out, "| %6d | %4d - %-5d | %7d - %-7d |\n",
462+
row.segmentLength,
463+
row.startSegmentCount, row.endSegmentCount,
464+
row.startSize, row.endSize,
465+
)
466+
}
467+
str := out.String()
468+
require.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(str))
469+
}
470+
471+
func checkNumIterations(t *testing.T, size uint32) {
472+
const numTrials = 20
473+
474+
keys := make([]uint64, size)
475+
var totalIterations, maxIterations int
476+
for range numTrials {
477+
for i := range keys {
478+
keys[i] = rand.Uint64()
479+
}
480+
var b BinaryFuseBuilder
481+
filter, iterations, err := buildBinaryFuse[uint8](&b, keys)
482+
require.NoError(t, err)
483+
for range 100 {
484+
require.True(t, filter.Contains(keys[rand.IntN(len(keys))]))
485+
}
486+
totalIterations += iterations
487+
maxIterations = max(maxIterations, iterations)
488+
}
489+
t.Logf("size: %d iterations: %.2f avg (%d max)", size, float64(totalIterations)/numTrials, maxIterations)
490+
}
491+
492+
func TestBinaryFuseBoundarySizes(t *testing.T) {
493+
// For each segment length, test the smallest and largest segment count. For a
494+
// given segment count, we want to choose the largest size for that count
495+
// (which has the least "slack" space).
496+
for _, s := range binaryFuseParamsTable() {
497+
if s.startSize > 1_000_000 {
498+
// Larger sizes take too long to test.
499+
break
500+
}
501+
if s.startSegmentCount != s.endSegmentCount {
502+
// Find the first size that doesn't use the start segment count.
503+
n := uint32(sort.Search(int(s.endSize-s.startSize+1), func(x int) bool {
504+
l, c := binaryFuseSegLenAndCnt(s.startSize + uint32(x))
505+
return l != s.segmentLength || c != s.startSegmentCount
506+
}))
507+
checkNumIterations(t, s.startSize+n-1)
508+
}
509+
checkNumIterations(t, s.endSize)
510+
}
511+
}

0 commit comments

Comments
 (0)