From c5e1b13fcc2d6ae3ea7919a27a163d33033c8d02 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Mon, 21 Sep 2026 10:28:34 -0400 Subject: [PATCH] Avoid tiny segment counts right after the segment length doubles (fixes #77) Just after binary_fuse_calculate_segment_length doubles the segment length, the filter could end up with very few segments (e.g. 12 segments of 1024 for 11.5k keys). Such a graph is close to a plain 3-wise XOR graph and the load was too near the peeling threshold: binary_fuse*_populate failed for 20-40% of random key sets around size 11,500 and ~3% around 12,350, with smaller but non-negligible failure rates after every doubling below ~40k keys. Halve the segment length until there are at least 32 segments (keeping segments >= 256). This only changes layouts for roughly 3.4k..58k keys, uses slightly less memory, and leaves construction and query speed unchanged. Add a regression test at the affected sizes. --- include/binaryfusefilter.h | 14 ++++++++++-- tests/unit.c | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/include/binaryfusefilter.h b/include/binaryfusefilter.h index 40a09c6..fef4b10 100644 --- a/include/binaryfusefilter.h +++ b/include/binaryfusefilter.h @@ -228,9 +228,14 @@ static inline bool binary_fuse8_allocate(uint32_t size, if (filter->SegmentLength > 262144) { filter->SegmentLength = 262144; } - filter->SegmentLengthMask = filter->SegmentLength - 1; double sizeFactor = size <= 1 ? 0 : binary_fuse_calculate_size_factor(arity, size); uint32_t capacity = size <= 1 ? 0 : (uint32_t)(round((double)size * sizeFactor)); + while (filter->SegmentLength > 256 && + (capacity + filter->SegmentLength - 1) / filter->SegmentLength < + 32 + (arity - 1)) { + filter->SegmentLength /= 2; + } + filter->SegmentLengthMask = filter->SegmentLength - 1; uint32_t initSegmentCount = (capacity + filter->SegmentLength - 1) / filter->SegmentLength - (arity - 1); @@ -524,9 +529,14 @@ static inline bool binary_fuse16_allocate(uint32_t size, if (filter->SegmentLength > 262144) { filter->SegmentLength = 262144; } - filter->SegmentLengthMask = filter->SegmentLength - 1; double sizeFactor = size <= 1 ? 0 : binary_fuse_calculate_size_factor(arity, size); uint32_t capacity = size <= 1 ? 0 : (uint32_t)(round((double)size * sizeFactor)); + while (filter->SegmentLength > 256 && + (capacity + filter->SegmentLength - 1) / filter->SegmentLength < + 32 + (arity - 1)) { + filter->SegmentLength /= 2; + } + filter->SegmentLengthMask = filter->SegmentLength - 1; uint32_t initSegmentCount = (capacity + filter->SegmentLength - 1) / filter->SegmentLength - (arity - 1); diff --git a/tests/unit.c b/tests/unit.c index abe5725..53a0eee 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -327,6 +327,50 @@ void failure_rate_binary_fuse16() { free(big_set); } +static uint64_t unit_splitmix64(uint64_t *seed) { + uint64_t z = (*seed += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +void small_segment_count_binary_fuse() { + printf("testing binary fuse at sizes with few segments (issue #77)\n"); + const uint32_t sizes[] = {3551, 11504, 11521, 12372, 13224, 37454}; + const size_t trials = 50; + uint64_t seed = 42; + for (size_t si = 0; si < sizeof(sizes) / sizeof(sizes[0]); si++) { + uint32_t size = sizes[si]; + uint64_t *big_set = (uint64_t *)malloc(sizeof(uint64_t) * size); + binary_fuse8_t filter8; + binary_fuse16_t filter16; + binary_fuse8_allocate(size, &filter8); + binary_fuse16_allocate(size, &filter16); + size_t failure = 0; + for (size_t trial = 0; trial < trials; trial++) { + for (size_t i = 0; i < size; i++) { + big_set[i] = unit_splitmix64(&seed); + } + if (!binary_fuse8_populate(big_set, size, &filter8)) { + failure++; + } + if (!binary_fuse16_populate(big_set, size, &filter16)) { + failure++; + } + } + printf("size %u: segment length %u, %u segments, %zu failures out of %zu\n", + size, filter8.SegmentLength, filter8.SegmentCount, failure, 2 * trials); + binary_fuse8_free(&filter8); + binary_fuse16_free(&filter16); + free(big_set); + if (failure > 0) { + printf("construction failed at size %u\n", size); + abort(); + } + } + printf("\n"); +} + // test code from the example in the README void readme_pack() { binary_fuse16_t filter = {0}; @@ -356,6 +400,7 @@ void readme_pack() { int main() { readme_pack(); failure_rate_binary_fuse16(); + small_segment_count_binary_fuse(); for(size_t size = 1000; size <= 1000000; size *= 300) { printf("== size = %zu \n", size); if(!testbinaryfuse8(size, 0)) { abort(); }