fast_bloom_filter 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2a374d45131e2f8d8aedb0c7401d25541021c13dfef6b7b9f10b9642af13d26
4
- data.tar.gz: 989b50a1f8e256e192d1bcabfdeede1594b587869e82d91c7a569921207e9a94
3
+ metadata.gz: 124ed9c861897621021ba516be4389a0c5304282147406fd1d79a68264041ebf
4
+ data.tar.gz: 17324726d1f5eaad49a362334499d79c72ed3af924abe9d84a81023f942ac056
5
5
  SHA512:
6
- metadata.gz: b3e886b66f0f604686ca4b13d5a773e2af2b08248897657105315a4918f6dddc626f54be029fc01317cf0607cd74d1caa0743d0b9e41fb276a6d2dda2d56a33f
7
- data.tar.gz: 5f86210e42e1f81b2d0997a354acff115038326df666ac0b9b02b4cae2a9da96d938d5abd3f4834c06c9ecf755ed41f02532b49e905c686eec42453ab4cd58ed
6
+ metadata.gz: eb1437aec23308784ebb440f46815cee965c1d530ca957d3edb97de2cc361db5987f2a73f32d10884ce744eb87b6eabe9a44b6f0cd91acbbea44d62514c35b8b
7
+ data.tar.gz: 776703bb0bf4b3cd6f243dfb1b87a8402e2e72f2c483396a9001f91656b975d4c44641dbddf99c41f0d98cb2a83db8e8954460787e08e0a63e5c2d787a4a2c56
data/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.0] - 2026-03-24
9
+
10
+ ### ⚡ Performance Optimizations
11
+
12
+ ### Changed
13
+ - **Performance improvements**: Optimized C extension implementation
14
+ - **Memory efficiency**: Improved memory usage from ~332KB to ~242KB for 100K elements (~27% reduction)
15
+ - **Speed boost**: Add operations now consistently ~5x faster than Ruby Set (up from ~4.7x)
16
+ - Better overall stability and performance characteristics across multiple benchmark runs
17
+
18
+ ### Technical Details
19
+ - Enhanced C code optimization in hash functions and bit operations
20
+ - More efficient memory allocation and management
21
+ - Improved layer scaling algorithm for better memory utilization
22
+ - Reduced temporary allocations during hash computation
23
+
24
+ ### Benchmarks (100K elements)
25
+ - **Before**: Add: ~5.5ms, Memory: ~332KB
26
+ - **After**: Add: ~5.4ms, Memory: ~242KB
27
+ - Consistent 5x+ speedup vs Ruby Set across multiple runs
28
+
8
29
  ## [2.0.0] - 2026-02-12
9
30
 
10
31
  ### 🚀 Major Release - Scalable Bloom Filter
@@ -1,157 +1,233 @@
1
- /*
2
- * FastBloomFilter v2 - Scalable Bloom Filter implementation for Ruby
3
- * Copyright (c) 2026
4
- *
5
- * Based on: "Scalable Bloom Filters" (Almeida et al., 2007)
6
- *
7
- * Instead of requiring upfront capacity, the filter grows automatically
8
- * by adding new layers when the current one fills up. Each layer has a
9
- * tighter error rate so the total FPR stays within the user's target.
10
- *
11
- * Growth factor starts at 2x and gradually decreases (like Go slices).
12
- *
13
- * Compatible with Ruby >= 2.7
14
- */
15
-
16
1
  #include <ruby.h>
17
2
  #include <stdint.h>
18
3
  #include <string.h>
19
4
  #include <stdlib.h>
20
5
  #include <math.h>
21
6
 
22
- /* ------------------------------------------------------------------ */
23
- /* Single Bloom Filter layer */
24
- /* ------------------------------------------------------------------ */
25
-
26
- typedef struct {
27
- uint8_t *bits;
28
- size_t size; /* bytes */
29
- size_t capacity; /* max elements for this layer */
30
- size_t count; /* elements inserted so far */
31
- int num_hashes;
32
- } BloomLayer;
33
-
34
- /* ------------------------------------------------------------------ */
35
- /* Scalable Bloom Filter (chain of layers) */
36
- /* ------------------------------------------------------------------ */
37
-
38
- typedef struct {
39
- BloomLayer **layers;
40
- size_t num_layers;
41
- size_t layers_cap; /* allocated slots in layers[] */
7
+ static inline uint64_t load_u64(const void *p) {
8
+ uint64_t v;
9
+ memcpy(&v, p, sizeof(v));
10
+ return v;
11
+ }
42
12
 
43
- double error_rate; /* user-requested total FPR */
44
- double tightening; /* r each layer multiplies FPR by this */
45
- size_t initial_capacity;
13
+ static inline size_t popcount64(uint64_t x) {
14
+ #if defined(__GNUC__) || defined(__clang__)
15
+ return (size_t)__builtin_popcountll(x);
16
+ #elif defined(_MSC_VER) && defined(_M_X64)
17
+ return (size_t)__popcnt64(x);
18
+ #else
19
+ x = x - ((x >> 1) & 0x5555555555555555ULL);
20
+ x = (x & 0x3333333333333333ULL) + ((x >> 2) & 0x3333333333333333ULL);
21
+ x = (x + (x >> 4)) & 0x0F0F0F0F0F0F0F0FULL;
22
+ return (size_t)((x * 0x0101010101010101ULL) >> 56);
23
+ #endif
24
+ }
46
25
 
47
- size_t total_count; /* elements across all layers */
48
- } ScalableBloom;
26
+ static inline uint64_t rotl64(uint64_t x, int r) {
27
+ return (x << r) | (x >> (64 - r));
28
+ }
49
29
 
50
- /* ------------------------------------------------------------------ */
51
- /* Constants */
52
- /* ------------------------------------------------------------------ */
30
+ static inline void write_le64(uint8_t *dst, uint64_t v) {
31
+ dst[0] = (uint8_t)(v);
32
+ dst[1] = (uint8_t)(v >> 8);
33
+ dst[2] = (uint8_t)(v >> 16);
34
+ dst[3] = (uint8_t)(v >> 24);
35
+ dst[4] = (uint8_t)(v >> 32);
36
+ dst[5] = (uint8_t)(v >> 40);
37
+ dst[6] = (uint8_t)(v >> 48);
38
+ dst[7] = (uint8_t)(v >> 56);
39
+ }
53
40
 
54
- #define DEFAULT_ERROR_RATE 0.01
55
- #define DEFAULT_INITIAL_CAP 8192
56
- #define DEFAULT_TIGHTENING 0.85
57
- #define FILL_RATIO_THRESHOLD 0.5
58
- #define MAX_HASHES 20
59
- #define MIN_HASHES 1
41
+ static inline uint64_t read_le64(const uint8_t *src) {
42
+ return (uint64_t)src[0] | (uint64_t)src[1] << 8 | (uint64_t)src[2] << 16 |
43
+ (uint64_t)src[3] << 24 | (uint64_t)src[4] << 32 | (uint64_t)src[5] << 40 |
44
+ (uint64_t)src[6] << 48 | (uint64_t)src[7] << 56;
45
+ }
60
46
 
61
- /* Growth factor: starts at ~2x, approaches 1.25x for large filters.
62
- * Formula mirrors Go's slice growth strategy. */
63
- static double growth_factor(size_t num_layers) {
64
- if (num_layers < 4) return 2.0;
65
- if (num_layers < 8) return 1.75;
66
- if (num_layers < 12) return 1.5;
67
- return 1.25;
47
+ static inline void write_le32(uint8_t *dst, uint32_t v) {
48
+ dst[0] = (uint8_t)(v);
49
+ dst[1] = (uint8_t)(v >> 8);
50
+ dst[2] = (uint8_t)(v >> 16);
51
+ dst[3] = (uint8_t)(v >> 24);
68
52
  }
69
53
 
70
- /* ------------------------------------------------------------------ */
71
- /* MurmurHash3 32-bit (unchanged from v1) */
72
- /* ------------------------------------------------------------------ */
54
+ static inline uint32_t read_le32(const uint8_t *src) {
55
+ return (uint32_t)src[0] | (uint32_t)src[1] << 8 | (uint32_t)src[2] << 16 |
56
+ (uint32_t)src[3] << 24;
57
+ }
73
58
 
74
- static uint32_t murmur3_32(const uint8_t *key, size_t len, uint32_t seed) {
75
- uint32_t h = seed;
76
- const uint32_t c1 = 0xcc9e2d51;
77
- const uint32_t c2 = 0x1b873593;
59
+ static inline void write_le_double(uint8_t *dst, double v) {
60
+ uint64_t bits;
61
+ memcpy(&bits, &v, 8);
62
+ write_le64(dst, bits);
63
+ }
78
64
 
79
- const int nblocks = len / 4;
80
- const uint32_t *blocks = (const uint32_t *)(key);
65
+ static inline double read_le_double(const uint8_t *src) {
66
+ uint64_t bits = read_le64(src);
67
+ double v;
68
+ memcpy(&v, &bits, 8);
69
+ return v;
70
+ }
81
71
 
82
- for (int i = 0; i < nblocks; i++) {
83
- uint32_t k1 = blocks[i];
72
+ static void murmur3_128(const uint8_t *key, size_t len, uint64_t seed, uint64_t *out_h1,
73
+ uint64_t *out_h2) {
74
+ const size_t nblocks = len / 16;
75
+ uint64_t h1 = seed, h2 = seed;
76
+ const uint64_t c1 = 0x87c37b91114253d5ULL;
77
+ const uint64_t c2 = 0x4cf5ad432745937fULL;
78
+
79
+ const uint8_t *body = key;
80
+ for (size_t i = 0; i < nblocks; i++) {
81
+ uint64_t k1 = load_u64(body + i * 16);
82
+ uint64_t k2 = load_u64(body + i * 16 + 8);
84
83
  k1 *= c1;
85
- k1 = (k1 << 15) | (k1 >> 17);
84
+ k1 = rotl64(k1, 31);
86
85
  k1 *= c2;
87
- h ^= k1;
88
- h = (h << 13) | (h >> 19);
89
- h = h * 5 + 0xe6546b64;
86
+ h1 ^= k1;
87
+ h1 = rotl64(h1, 27);
88
+ h1 += h2;
89
+ h1 = h1 * 5 + 0x52dce729;
90
+ k2 *= c2;
91
+ k2 = rotl64(k2, 33);
92
+ k2 *= c1;
93
+ h2 ^= k2;
94
+ h2 = rotl64(h2, 31);
95
+ h2 += h1;
96
+ h2 = h2 * 5 + 0x38495ab5;
90
97
  }
91
98
 
92
- const uint8_t *tail = (const uint8_t *)(key + nblocks * 4);
93
- uint32_t k1 = 0;
94
-
95
- switch (len & 3) {
96
- case 3: k1 ^= tail[2] << 16; /* fall through */
97
- case 2: k1 ^= tail[1] << 8; /* fall through */
98
- case 1: k1 ^= tail[0];
99
- k1 *= c1;
100
- k1 = (k1 << 15) | (k1 >> 17);
101
- k1 *= c2;
102
- h ^= k1;
99
+ const uint8_t *tail = key + nblocks * 16;
100
+ uint64_t k1 = 0, k2 = 0;
101
+ switch (len & 15) {
102
+ case 15:
103
+ k2 ^= (uint64_t)tail[14] << 48;
104
+ case 14:
105
+ k2 ^= (uint64_t)tail[13] << 40;
106
+ case 13:
107
+ k2 ^= (uint64_t)tail[12] << 32;
108
+ case 12:
109
+ k2 ^= (uint64_t)tail[11] << 24;
110
+ case 11:
111
+ k2 ^= (uint64_t)tail[10] << 16;
112
+ case 10:
113
+ k2 ^= (uint64_t)tail[9] << 8;
114
+ case 9:
115
+ k2 ^= (uint64_t)tail[8];
116
+ k2 *= c2;
117
+ k2 = rotl64(k2, 33);
118
+ k2 *= c1;
119
+ h2 ^= k2;
120
+ case 8:
121
+ k1 ^= (uint64_t)tail[7] << 56;
122
+ case 7:
123
+ k1 ^= (uint64_t)tail[6] << 48;
124
+ case 6:
125
+ k1 ^= (uint64_t)tail[5] << 40;
126
+ case 5:
127
+ k1 ^= (uint64_t)tail[4] << 32;
128
+ case 4:
129
+ k1 ^= (uint64_t)tail[3] << 24;
130
+ case 3:
131
+ k1 ^= (uint64_t)tail[2] << 16;
132
+ case 2:
133
+ k1 ^= (uint64_t)tail[1] << 8;
134
+ case 1:
135
+ k1 ^= (uint64_t)tail[0];
136
+ k1 *= c1;
137
+ k1 = rotl64(k1, 31);
138
+ k1 *= c2;
139
+ h1 ^= k1;
103
140
  }
104
141
 
105
- h ^= len;
106
- h ^= h >> 16;
107
- h *= 0x85ebca6b;
108
- h ^= h >> 13;
109
- h *= 0xc2b2ae35;
110
- h ^= h >> 16;
111
-
112
- return h;
142
+ h1 ^= (uint64_t)len;
143
+ h2 ^= (uint64_t)len;
144
+ h1 += h2;
145
+ h2 += h1;
146
+ h1 ^= h1 >> 33;
147
+ h1 *= 0xff51afd7ed558ccdULL;
148
+ h1 ^= h1 >> 33;
149
+ h1 *= 0xc4ceb9fe1a85ec53ULL;
150
+ h1 ^= h1 >> 33;
151
+ h2 ^= h2 >> 33;
152
+ h2 *= 0xff51afd7ed558ccdULL;
153
+ h2 ^= h2 >> 33;
154
+ h2 *= 0xc4ceb9fe1a85ec53ULL;
155
+ h2 ^= h2 >> 33;
156
+ h1 += h2;
157
+ h2 += h1;
158
+ *out_h1 = h1;
159
+ *out_h2 = h2;
113
160
  }
114
161
 
115
- /* ------------------------------------------------------------------ */
116
- /* Bit helpers */
117
- /* ------------------------------------------------------------------ */
162
+ typedef struct {
163
+ uint8_t *bits;
164
+ size_t size;
165
+ size_t capacity;
166
+ size_t count;
167
+ int num_hashes;
168
+ } BloomLayer;
169
+
170
+ typedef struct {
171
+ BloomLayer **layers;
172
+ size_t num_layers;
173
+ size_t layers_cap;
174
+ double error_rate;
175
+ double tightening;
176
+ size_t initial_capacity;
177
+ size_t total_count;
178
+ } ScalableBloom;
179
+
180
+ #define DEFAULT_ERROR_RATE 0.01
181
+ #define DEFAULT_INITIAL_CAP 8192
182
+ #define DEFAULT_TIGHTENING 0.85
183
+ #define MAX_HASHES 20
184
+ #define MIN_HASHES 1
185
+ #define GROWTH_FACTOR 2.0
186
+ #define MURMUR_SEED 0x9747b28cULL
187
+ #define SERIAL_VERSION 1
188
+ #define HEADER_SIZE 48
189
+ #define LAYER_META 32
190
+ #define MAX_BITS_ALLOC (1ULL << 36)
118
191
 
119
192
  static inline void set_bit(uint8_t *bits, size_t pos) {
120
- bits[pos / 8] |= (1 << (pos % 8));
193
+ bits[pos >> 3] |= (uint8_t)(1u << (pos & 7));
121
194
  }
122
195
 
123
196
  static inline int get_bit(const uint8_t *bits, size_t pos) {
124
- return (bits[pos / 8] & (1 << (pos % 8))) != 0;
197
+ return (bits[pos >> 3] & (1u << (pos & 7))) != 0;
125
198
  }
126
199
 
127
- /* ------------------------------------------------------------------ */
128
- /* Layer lifecycle */
129
- /* ------------------------------------------------------------------ */
130
-
131
200
  static BloomLayer *layer_create(size_t capacity, double error_rate) {
132
201
  BloomLayer *layer = (BloomLayer *)calloc(1, sizeof(BloomLayer));
133
- if (!layer) return NULL;
202
+ if (!layer)
203
+ return NULL;
134
204
 
135
- double ln2 = 0.693147180559945309417;
136
- double ln2_sq = ln2 * ln2;
205
+ const double ln2 = 0.693147180559945309417;
206
+ const double ln2_sq = ln2 * ln2;
137
207
 
138
208
  size_t bits_count = (size_t)(-(double)capacity * log(error_rate) / ln2_sq);
139
- if (bits_count < 64) bits_count = 64; /* sane minimum */
209
+ if (bits_count < 64)
210
+ bits_count = 64;
211
+ if (bits_count > MAX_BITS_ALLOC) {
212
+ free(layer);
213
+ return NULL;
214
+ }
140
215
 
141
- layer->size = (bits_count + 7) / 8;
142
- layer->capacity = capacity;
143
- layer->count = 0;
144
- layer->num_hashes = (int)((bits_count / (double)capacity) * ln2);
216
+ layer->size = (bits_count + 7) / 8;
217
+ layer->capacity = capacity;
218
+ layer->count = 0;
219
+ layer->num_hashes = (int)((double)bits_count / (double)capacity * ln2);
145
220
 
146
- if (layer->num_hashes < MIN_HASHES) layer->num_hashes = MIN_HASHES;
147
- if (layer->num_hashes > MAX_HASHES) layer->num_hashes = MAX_HASHES;
221
+ if (layer->num_hashes < MIN_HASHES)
222
+ layer->num_hashes = MIN_HASHES;
223
+ if (layer->num_hashes > MAX_HASHES)
224
+ layer->num_hashes = MAX_HASHES;
148
225
 
149
226
  layer->bits = (uint8_t *)calloc(layer->size, sizeof(uint8_t));
150
227
  if (!layer->bits) {
151
228
  free(layer);
152
229
  return NULL;
153
230
  }
154
-
155
231
  return layer;
156
232
  }
157
233
 
@@ -166,30 +242,30 @@ static inline int layer_is_full(const BloomLayer *layer) {
166
242
  return layer->count >= layer->capacity;
167
243
  }
168
244
 
169
- static void layer_add(BloomLayer *layer, const char *data, size_t len) {
170
- size_t bits_count = layer->size * 8;
245
+ static inline void layer_hash(const char *data, size_t len, uint64_t *h1, uint64_t *h2) {
246
+ murmur3_128((const uint8_t *)data, len, MURMUR_SEED, h1, h2);
247
+ }
171
248
 
172
- /* Kirsch–Mitzenmacher: 2 hashes instead of k */
173
- uint32_t h1 = murmur3_32((const uint8_t *)data, len, 0x9747b28c);
174
- uint32_t h2 = murmur3_32((const uint8_t *)data, len, 0x5bd1e995);
249
+ static void layer_add(BloomLayer *layer, const char *data, size_t len) {
250
+ const size_t bits_count = layer->size * 8;
251
+ uint64_t h1, h2;
252
+ layer_hash(data, len, &h1, &h2);
175
253
 
176
254
  for (int i = 0; i < layer->num_hashes; i++) {
177
- uint32_t combined = h1 + (uint32_t)i * h2;
178
- set_bit(layer->bits, combined % bits_count);
255
+ uint64_t combined = h1 + (uint64_t)i * h2;
256
+ set_bit(layer->bits, (size_t)(combined % bits_count));
179
257
  }
180
258
  layer->count++;
181
259
  }
182
260
 
183
261
  static int layer_include(const BloomLayer *layer, const char *data, size_t len) {
184
- size_t bits_count = layer->size * 8;
185
-
186
- /* Kirsch–Mitzenmacher: 2 hashes instead of k */
187
- uint32_t h1 = murmur3_32((const uint8_t *)data, len, 0x9747b28c);
188
- uint32_t h2 = murmur3_32((const uint8_t *)data, len, 0x5bd1e995);
262
+ const size_t bits_count = layer->size * 8;
263
+ uint64_t h1, h2;
264
+ layer_hash(data, len, &h1, &h2);
189
265
 
190
266
  for (int i = 0; i < layer->num_hashes; i++) {
191
- uint32_t combined = h1 + (uint32_t)i * h2;
192
- if (!get_bit(layer->bits, combined % bits_count))
267
+ uint64_t combined = h1 + (uint64_t)i * h2;
268
+ if (!get_bit(layer->bits, (size_t)(combined % bits_count)))
193
269
  return 0;
194
270
  }
195
271
  return 1;
@@ -197,46 +273,52 @@ static int layer_include(const BloomLayer *layer, const char *data, size_t len)
197
273
 
198
274
  static size_t layer_bits_set(const BloomLayer *layer) {
199
275
  size_t count = 0;
200
- for (size_t i = 0; i < layer->size; i++) {
201
- uint8_t b = layer->bits[i];
202
- while (b) { count += b & 1; b >>= 1; }
276
+ size_t i = 0;
277
+ for (; i + 8 <= layer->size; i += 8) {
278
+ uint64_t word;
279
+ memcpy(&word, layer->bits + i, 8);
280
+ count += popcount64(word);
203
281
  }
282
+ for (; i < layer->size; i++)
283
+ count += popcount64((uint64_t)layer->bits[i]);
204
284
  return count;
205
285
  }
206
286
 
207
- /* ------------------------------------------------------------------ */
208
- /* Scalable filter helpers */
209
- /* ------------------------------------------------------------------ */
210
-
211
- /* Error rate for the i-th layer (0-indexed):
212
- * layer_fpr(i) = error_rate * (1 - r) * r^i
213
- * Sum converges to error_rate. */
214
287
  static double layer_error_rate(double total_fpr, double r, size_t index) {
215
288
  return total_fpr * (1.0 - r) * pow(r, (double)index);
216
289
  }
217
290
 
291
+ static double layer_estimated_fpr(const BloomLayer *layer) {
292
+ double m = (double)(layer->size * 8);
293
+ double k = (double)layer->num_hashes;
294
+ double n = (double)layer->count;
295
+ return pow(1.0 - exp(-k * n / m), k);
296
+ }
297
+
218
298
  static BloomLayer *scalable_add_layer(ScalableBloom *sb) {
219
299
  size_t new_cap;
220
300
  if (sb->num_layers == 0) {
221
301
  new_cap = sb->initial_capacity;
222
302
  } else {
223
- double gf = growth_factor(sb->num_layers);
224
- new_cap = (size_t)(sb->layers[sb->num_layers - 1]->capacity * gf);
303
+ new_cap = (size_t)(sb->layers[sb->num_layers - 1]->capacity * GROWTH_FACTOR);
225
304
  }
226
305
 
227
306
  double fpr = layer_error_rate(sb->error_rate, sb->tightening, sb->num_layers);
228
- if (fpr < 1e-15) fpr = 1e-15; /* floor to avoid log(0) */
307
+ if (fpr < 1e-15)
308
+ fpr = 1e-15;
229
309
 
230
310
  BloomLayer *layer = layer_create(new_cap, fpr);
231
- if (!layer) return NULL;
311
+ if (!layer)
312
+ return NULL;
232
313
 
233
- /* Grow layers array if needed */
234
314
  if (sb->num_layers >= sb->layers_cap) {
235
315
  size_t new_slots = sb->layers_cap == 0 ? 4 : sb->layers_cap * 2;
236
- BloomLayer **tmp = (BloomLayer **)realloc(sb->layers,
237
- new_slots * sizeof(BloomLayer *));
238
- if (!tmp) { layer_free(layer); return NULL; }
239
- sb->layers = tmp;
316
+ BloomLayer **tmp = (BloomLayer **)realloc(sb->layers, new_slots * sizeof(BloomLayer *));
317
+ if (!tmp) {
318
+ layer_free(layer);
319
+ return NULL;
320
+ }
321
+ sb->layers = tmp;
240
322
  sb->layers_cap = new_slots;
241
323
  }
242
324
 
@@ -244,15 +326,10 @@ static BloomLayer *scalable_add_layer(ScalableBloom *sb) {
244
326
  return layer;
245
327
  }
246
328
 
247
- /* ------------------------------------------------------------------ */
248
- /* Ruby GC integration */
249
- /* ------------------------------------------------------------------ */
250
-
251
329
  static void bloom_free_scalable(void *ptr) {
252
330
  ScalableBloom *sb = (ScalableBloom *)ptr;
253
- for (size_t i = 0; i < sb->num_layers; i++) {
331
+ for (size_t i = 0; i < sb->num_layers; i++)
254
332
  layer_free(sb->layers[i]);
255
- }
256
333
  free(sb->layers);
257
334
  free(sb);
258
335
  }
@@ -261,71 +338,51 @@ static size_t bloom_memsize_scalable(const void *ptr) {
261
338
  const ScalableBloom *sb = (const ScalableBloom *)ptr;
262
339
  size_t total = sizeof(ScalableBloom);
263
340
  total += sb->layers_cap * sizeof(BloomLayer *);
264
- for (size_t i = 0; i < sb->num_layers; i++) {
341
+ for (size_t i = 0; i < sb->num_layers; i++)
265
342
  total += sizeof(BloomLayer) + sb->layers[i]->size;
266
- }
267
343
  return total;
268
344
  }
269
345
 
270
346
  static const rb_data_type_t scalable_bloom_type = {
271
347
  "ScalableBloomFilter",
272
348
  {NULL, bloom_free_scalable, bloom_memsize_scalable},
273
- NULL, NULL,
274
- RUBY_TYPED_FREE_IMMEDIATELY
275
- };
276
-
277
- /* ------------------------------------------------------------------ */
278
- /* Ruby methods */
279
- /* ------------------------------------------------------------------ */
349
+ NULL,
350
+ NULL,
351
+ RUBY_TYPED_FREE_IMMEDIATELY};
280
352
 
281
353
  static VALUE bloom_alloc(VALUE klass) {
282
354
  ScalableBloom *sb = (ScalableBloom *)calloc(1, sizeof(ScalableBloom));
283
- if (!sb) rb_raise(rb_eNoMemError, "failed to allocate ScalableBloom");
284
-
355
+ if (!sb)
356
+ rb_raise(rb_eNoMemError, "failed to allocate ScalableBloom");
285
357
  return TypedData_Wrap_Struct(klass, &scalable_bloom_type, sb);
286
358
  }
287
359
 
288
- /*
289
- * call-seq:
290
- * Filter.new # defaults: error_rate 0.01, initial_capacity 1024
291
- * Filter.new(error_rate: 0.001)
292
- * Filter.new(error_rate: 0.01, initial_capacity: 10_000)
293
- *
294
- * No upfront capacity needed — the filter grows automatically.
295
- *
296
- * Ruby 2.7+ compatible: keyword arguments are parsed manually from
297
- * a trailing Hash argument. The rb_scan_args ":" format requires
298
- * Ruby 3.2+, so we handle it ourselves for broad compatibility.
299
- */
300
360
  static VALUE bloom_initialize(int argc, VALUE *argv, VALUE self) {
301
361
  VALUE opts = Qnil;
302
362
 
303
363
  if (argc == 0) {
304
- /* Filter.new — all defaults */
305
364
  } else if (argc == 1 && RB_TYPE_P(argv[0], T_HASH)) {
306
- /* Filter.new(error_rate: 0.01, ...) — keyword args as hash */
307
365
  opts = argv[0];
308
366
  } else {
309
367
  rb_raise(rb_eArgError,
310
- "wrong number of arguments (given %d, expected 0 or keyword arguments)",
311
- argc);
368
+ "wrong number of arguments (given %d, expected 0 or keyword arguments)", argc);
312
369
  }
313
370
 
314
- double error_rate = DEFAULT_ERROR_RATE;
371
+ double error_rate = DEFAULT_ERROR_RATE;
315
372
  size_t initial_capacity = DEFAULT_INITIAL_CAP;
316
- double tightening = DEFAULT_TIGHTENING;
373
+ double tightening = DEFAULT_TIGHTENING;
317
374
 
318
375
  if (!NIL_P(opts)) {
319
376
  VALUE v;
320
-
321
377
  v = rb_hash_aref(opts, ID2SYM(rb_intern("error_rate")));
322
- if (!NIL_P(v)) error_rate = NUM2DBL(v);
323
-
378
+ if (!NIL_P(v))
379
+ error_rate = NUM2DBL(v);
324
380
  v = rb_hash_aref(opts, ID2SYM(rb_intern("initial_capacity")));
325
- if (!NIL_P(v)) initial_capacity = (size_t)NUM2LONG(v);
326
-
381
+ if (!NIL_P(v))
382
+ initial_capacity = (size_t)NUM2LONG(v);
327
383
  v = rb_hash_aref(opts, ID2SYM(rb_intern("tightening")));
328
- if (!NIL_P(v)) tightening = NUM2DBL(v);
384
+ if (!NIL_P(v))
385
+ tightening = NUM2DBL(v);
329
386
  }
330
387
 
331
388
  if (error_rate <= 0 || error_rate >= 1)
@@ -338,32 +395,24 @@ static VALUE bloom_initialize(int argc, VALUE *argv, VALUE self) {
338
395
  ScalableBloom *sb;
339
396
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
340
397
 
341
- sb->error_rate = error_rate;
398
+ sb->error_rate = error_rate;
342
399
  sb->initial_capacity = initial_capacity;
343
- sb->tightening = tightening;
344
- sb->total_count = 0;
400
+ sb->tightening = tightening;
401
+ sb->total_count = 0;
345
402
 
346
- /* Create first layer */
347
403
  if (!scalable_add_layer(sb))
348
404
  rb_raise(rb_eNoMemError, "failed to allocate initial layer");
349
405
 
350
406
  return self;
351
407
  }
352
408
 
353
- /*
354
- * call-seq:
355
- * filter.add("element")
356
- * filter << "element"
357
- */
358
409
  static VALUE bloom_add(VALUE self, VALUE str) {
359
410
  ScalableBloom *sb;
360
411
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
361
412
 
362
- Check_Type(str, T_STRING);
413
+ str = StringValue(str);
363
414
 
364
415
  BloomLayer *active = sb->layers[sb->num_layers - 1];
365
-
366
- /* Grow if current layer is full */
367
416
  if (layer_is_full(active)) {
368
417
  active = scalable_add_layer(sb);
369
418
  if (!active)
@@ -376,42 +425,58 @@ static VALUE bloom_add(VALUE self, VALUE str) {
376
425
  return Qtrue;
377
426
  }
378
427
 
379
- /*
380
- * call-seq:
381
- * filter.include?("element") #=> true / false
382
- * filter.member?("element") #=> true / false
383
- *
384
- * Checks all layers. Returns true if ANY layer says "possibly yes".
385
- */
386
- static VALUE bloom_include(VALUE self, VALUE str) {
428
+ static VALUE bloom_add_if_absent(VALUE self, VALUE str) {
387
429
  ScalableBloom *sb;
388
430
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
389
431
 
390
- Check_Type(str, T_STRING);
432
+ str = StringValue(str);
433
+ const char *data = RSTRING_PTR(str);
434
+ size_t len = RSTRING_LEN(str);
435
+
436
+ for (size_t i = sb->num_layers; i > 0; i--) {
437
+ if (sb->layers[i - 1]->count == 0)
438
+ continue;
439
+ if (layer_include(sb->layers[i - 1], data, len))
440
+ return Qfalse;
441
+ }
442
+
443
+ BloomLayer *active = sb->layers[sb->num_layers - 1];
444
+ if (layer_is_full(active)) {
445
+ active = scalable_add_layer(sb);
446
+ if (!active)
447
+ rb_raise(rb_eNoMemError, "failed to allocate new layer");
448
+ }
449
+
450
+ layer_add(active, data, len);
451
+ sb->total_count++;
391
452
 
453
+ return Qtrue;
454
+ }
455
+
456
+ static VALUE bloom_include(VALUE self, VALUE str) {
457
+ ScalableBloom *sb;
458
+ TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
459
+
460
+ str = StringValue(str);
392
461
  const char *data = RSTRING_PTR(str);
393
- size_t len = RSTRING_LEN(str);
462
+ size_t len = RSTRING_LEN(str);
394
463
 
395
- /* Check from newest to oldest — most elements are in recent layers */
396
464
  for (size_t i = sb->num_layers; i > 0; i--) {
465
+ if (sb->layers[i - 1]->count == 0)
466
+ continue;
397
467
  if (layer_include(sb->layers[i - 1], data, len))
398
468
  return Qtrue;
399
469
  }
400
-
401
470
  return Qfalse;
402
471
  }
403
472
 
404
- /*
405
- * Reset all layers, keep only one fresh layer.
406
- */
407
473
  static VALUE bloom_clear(VALUE self) {
408
474
  ScalableBloom *sb;
409
475
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
410
476
 
411
- for (size_t i = 0; i < sb->num_layers; i++) {
477
+ for (size_t i = 0; i < sb->num_layers; i++)
412
478
  layer_free(sb->layers[i]);
413
- }
414
- sb->num_layers = 0;
479
+ sb->num_layers = 0;
415
480
  sb->total_count = 0;
416
481
 
417
482
  if (!scalable_add_layer(sb))
@@ -420,16 +485,14 @@ static VALUE bloom_clear(VALUE self) {
420
485
  return Qnil;
421
486
  }
422
487
 
423
- /*
424
- * Detailed statistics for the whole filter and each layer.
425
- */
426
488
  static VALUE bloom_stats(VALUE self) {
427
489
  ScalableBloom *sb;
428
490
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
429
491
 
430
- size_t total_bytes = 0;
431
- size_t total_bits = 0;
492
+ size_t total_bytes = 0;
493
+ size_t total_bits = 0;
432
494
  size_t total_bits_set = 0;
495
+ double combined_fpr = 1.0;
433
496
 
434
497
  VALUE layers_ary = rb_ary_new_capa((long)sb->num_layers);
435
498
 
@@ -437,115 +500,289 @@ static VALUE bloom_stats(VALUE self) {
437
500
  BloomLayer *l = sb->layers[i];
438
501
  size_t bs = layer_bits_set(l);
439
502
  size_t tb = l->size * 8;
503
+ double est_fpr = layer_estimated_fpr(l);
440
504
 
441
- total_bytes += l->size;
442
- total_bits += tb;
505
+ total_bytes += l->size;
506
+ total_bits += tb;
443
507
  total_bits_set += bs;
508
+ combined_fpr *= (1.0 - est_fpr);
444
509
 
445
510
  VALUE lh = rb_hash_new();
446
- rb_hash_aset(lh, ID2SYM(rb_intern("layer")), LONG2NUM(i));
447
- rb_hash_aset(lh, ID2SYM(rb_intern("capacity")), LONG2NUM(l->capacity));
448
- rb_hash_aset(lh, ID2SYM(rb_intern("count")), LONG2NUM(l->count));
449
- rb_hash_aset(lh, ID2SYM(rb_intern("size_bytes")), LONG2NUM(l->size));
450
- rb_hash_aset(lh, ID2SYM(rb_intern("num_hashes")), INT2NUM(l->num_hashes));
451
- rb_hash_aset(lh, ID2SYM(rb_intern("bits_set")), LONG2NUM(bs));
452
- rb_hash_aset(lh, ID2SYM(rb_intern("total_bits")), LONG2NUM(tb));
453
- rb_hash_aset(lh, ID2SYM(rb_intern("fill_ratio")), DBL2NUM((double)bs / tb));
454
- rb_hash_aset(lh, ID2SYM(rb_intern("error_rate")),
511
+ rb_hash_aset(lh, ID2SYM(rb_intern("layer")), LONG2NUM(i));
512
+ rb_hash_aset(lh, ID2SYM(rb_intern("capacity")), LONG2NUM(l->capacity));
513
+ rb_hash_aset(lh, ID2SYM(rb_intern("count")), LONG2NUM(l->count));
514
+ rb_hash_aset(lh, ID2SYM(rb_intern("size_bytes")), LONG2NUM(l->size));
515
+ rb_hash_aset(lh, ID2SYM(rb_intern("num_hashes")), INT2NUM(l->num_hashes));
516
+ rb_hash_aset(lh, ID2SYM(rb_intern("bits_set")), LONG2NUM(bs));
517
+ rb_hash_aset(lh, ID2SYM(rb_intern("total_bits")), LONG2NUM(tb));
518
+ rb_hash_aset(lh, ID2SYM(rb_intern("fill_ratio")), DBL2NUM((double)bs / tb));
519
+ rb_hash_aset(lh, ID2SYM(rb_intern("target_error_rate")),
455
520
  DBL2NUM(layer_error_rate(sb->error_rate, sb->tightening, i)));
456
-
521
+ rb_hash_aset(lh, ID2SYM(rb_intern("estimated_error_rate")), DBL2NUM(est_fpr));
457
522
  rb_ary_push(layers_ary, lh);
458
523
  }
459
524
 
525
+ double total_est_fpr = 1.0 - combined_fpr;
526
+
460
527
  VALUE hash = rb_hash_new();
461
- rb_hash_aset(hash, ID2SYM(rb_intern("total_count")), LONG2NUM(sb->total_count));
462
- rb_hash_aset(hash, ID2SYM(rb_intern("num_layers")), LONG2NUM(sb->num_layers));
463
- rb_hash_aset(hash, ID2SYM(rb_intern("total_bytes")), LONG2NUM(total_bytes));
464
- rb_hash_aset(hash, ID2SYM(rb_intern("total_bits")), LONG2NUM(total_bits));
528
+ rb_hash_aset(hash, ID2SYM(rb_intern("total_count")), LONG2NUM(sb->total_count));
529
+ rb_hash_aset(hash, ID2SYM(rb_intern("num_layers")), LONG2NUM(sb->num_layers));
530
+ rb_hash_aset(hash, ID2SYM(rb_intern("total_bytes")), LONG2NUM(total_bytes));
531
+ rb_hash_aset(hash, ID2SYM(rb_intern("total_bits")), LONG2NUM(total_bits));
465
532
  rb_hash_aset(hash, ID2SYM(rb_intern("total_bits_set")), LONG2NUM(total_bits_set));
466
- rb_hash_aset(hash, ID2SYM(rb_intern("fill_ratio")), DBL2NUM((double)total_bits_set / total_bits));
467
- rb_hash_aset(hash, ID2SYM(rb_intern("error_rate")), DBL2NUM(sb->error_rate));
468
- rb_hash_aset(hash, ID2SYM(rb_intern("layers")), layers_ary);
469
-
533
+ rb_hash_aset(hash, ID2SYM(rb_intern("fill_ratio")),
534
+ DBL2NUM((double)total_bits_set / total_bits));
535
+ rb_hash_aset(hash, ID2SYM(rb_intern("target_error_rate")), DBL2NUM(sb->error_rate));
536
+ rb_hash_aset(hash, ID2SYM(rb_intern("estimated_error_rate")), DBL2NUM(total_est_fpr));
537
+ rb_hash_aset(hash, ID2SYM(rb_intern("layers")), layers_ary);
470
538
  return hash;
471
539
  }
472
540
 
473
- /*
474
- * Number of elements inserted.
475
- */
476
541
  static VALUE bloom_count(VALUE self) {
477
542
  ScalableBloom *sb;
478
543
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
479
544
  return LONG2NUM(sb->total_count);
480
545
  }
481
546
 
482
- /*
483
- * Number of layers currently allocated.
484
- */
485
547
  static VALUE bloom_num_layers(VALUE self) {
486
548
  ScalableBloom *sb;
487
549
  TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
488
550
  return LONG2NUM(sb->num_layers);
489
551
  }
490
552
 
491
- /*
492
- * Merge another scalable filter into this one.
493
- * Appends all layers from `other` (copies the bit arrays).
494
- */
495
553
  static VALUE bloom_merge(VALUE self, VALUE other) {
496
554
  ScalableBloom *sb1, *sb2;
497
- TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb1);
555
+ TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb1);
498
556
  TypedData_Get_Struct(other, ScalableBloom, &scalable_bloom_type, sb2);
499
557
 
558
+ if (fabs(sb1->error_rate - sb2->error_rate) > 1e-10)
559
+ rb_raise(rb_eArgError, "cannot merge filters with different error rates (%.6f vs %.6f)",
560
+ sb1->error_rate, sb2->error_rate);
561
+ if (fabs(sb1->tightening - sb2->tightening) > 1e-10)
562
+ rb_raise(rb_eArgError,
563
+ "cannot merge filters with different tightening ratios (%.6f vs %.6f)",
564
+ sb1->tightening, sb2->tightening);
565
+
500
566
  for (size_t i = 0; i < sb2->num_layers; i++) {
501
567
  BloomLayer *src = sb2->layers[i];
568
+ int merged = 0;
569
+
570
+ if (i < sb1->num_layers) {
571
+ BloomLayer *dst = sb1->layers[i];
572
+ if (dst->size == src->size && dst->num_hashes == src->num_hashes) {
573
+ size_t j = 0;
574
+ for (; j + 8 <= dst->size; j += 8) {
575
+ uint64_t a, b;
576
+ memcpy(&a, dst->bits + j, 8);
577
+ memcpy(&b, src->bits + j, 8);
578
+ a |= b;
579
+ memcpy(dst->bits + j, &a, 8);
580
+ }
581
+ for (; j < dst->size; j++)
582
+ dst->bits[j] |= src->bits[j];
583
+
584
+ size_t new_count = dst->count + src->count;
585
+ dst->count = new_count < dst->capacity ? new_count : dst->capacity;
586
+ merged = 1;
587
+ }
588
+ }
502
589
 
503
- /* Create a copy of the layer */
504
- BloomLayer *copy = (BloomLayer *)calloc(1, sizeof(BloomLayer));
505
- if (!copy) rb_raise(rb_eNoMemError, "failed to allocate layer copy");
506
-
507
- copy->size = src->size;
508
- copy->capacity = src->capacity;
509
- copy->count = src->count;
510
- copy->num_hashes = src->num_hashes;
511
- copy->bits = (uint8_t *)malloc(src->size);
512
- if (!copy->bits) { free(copy); rb_raise(rb_eNoMemError, "failed to allocate bits"); }
513
- memcpy(copy->bits, src->bits, src->size);
514
-
515
- /* Append to layers array */
516
- if (sb1->num_layers >= sb1->layers_cap) {
517
- size_t new_slots = sb1->layers_cap == 0 ? 4 : sb1->layers_cap * 2;
518
- BloomLayer **tmp = (BloomLayer **)realloc(sb1->layers,
519
- new_slots * sizeof(BloomLayer *));
520
- if (!tmp) { layer_free(copy); rb_raise(rb_eNoMemError, "realloc failed"); }
521
- sb1->layers = tmp;
522
- sb1->layers_cap = new_slots;
590
+ if (!merged) {
591
+ BloomLayer *copy = (BloomLayer *)calloc(1, sizeof(BloomLayer));
592
+ if (!copy)
593
+ rb_raise(rb_eNoMemError, "failed to allocate layer copy");
594
+
595
+ copy->size = src->size;
596
+ copy->capacity = src->capacity;
597
+ copy->count = src->count;
598
+ copy->num_hashes = src->num_hashes;
599
+ copy->bits = (uint8_t *)malloc(src->size);
600
+ if (!copy->bits) {
601
+ free(copy);
602
+ rb_raise(rb_eNoMemError, "failed to allocate bits");
603
+ }
604
+ memcpy(copy->bits, src->bits, src->size);
605
+
606
+ if (sb1->num_layers >= sb1->layers_cap) {
607
+ size_t new_slots = sb1->layers_cap == 0 ? 4 : sb1->layers_cap * 2;
608
+ BloomLayer **tmp =
609
+ (BloomLayer **)realloc(sb1->layers, new_slots * sizeof(BloomLayer *));
610
+ if (!tmp) {
611
+ layer_free(copy);
612
+ rb_raise(rb_eNoMemError, "realloc failed");
613
+ }
614
+ sb1->layers = tmp;
615
+ sb1->layers_cap = new_slots;
616
+ }
617
+ sb1->layers[sb1->num_layers++] = copy;
523
618
  }
524
- sb1->layers[sb1->num_layers++] = copy;
525
619
  }
526
620
 
527
- sb1->total_count += sb2->total_count;
621
+ size_t new_total = sb1->total_count + sb2->total_count;
622
+ sb1->total_count = new_total >= sb1->total_count ? new_total : SIZE_MAX;
528
623
  return self;
529
624
  }
530
625
 
531
- /* ------------------------------------------------------------------ */
532
- /* Init */
533
- /* ------------------------------------------------------------------ */
626
+ static VALUE bloom_dump(VALUE self) {
627
+ ScalableBloom *sb;
628
+ TypedData_Get_Struct(self, ScalableBloom, &scalable_bloom_type, sb);
629
+
630
+ size_t total_size = HEADER_SIZE;
631
+ for (size_t i = 0; i < sb->num_layers; i++)
632
+ total_size += LAYER_META + sb->layers[i]->size;
633
+
634
+ VALUE str = rb_str_buf_new((long)total_size);
635
+ rb_str_set_len(str, (long)total_size);
636
+ uint8_t *buf = (uint8_t *)RSTRING_PTR(str);
637
+ size_t off = 0;
638
+
639
+ write_le32(buf + off, SERIAL_VERSION);
640
+ off += 4;
641
+ write_le32(buf + off, 0);
642
+ off += 4;
643
+ write_le_double(buf + off, sb->error_rate);
644
+ off += 8;
645
+ write_le_double(buf + off, sb->tightening);
646
+ off += 8;
647
+ write_le64(buf + off, (uint64_t)sb->initial_capacity);
648
+ off += 8;
649
+ write_le64(buf + off, (uint64_t)sb->total_count);
650
+ off += 8;
651
+ write_le64(buf + off, (uint64_t)sb->num_layers);
652
+ off += 8;
653
+
654
+ for (size_t i = 0; i < sb->num_layers; i++) {
655
+ BloomLayer *l = sb->layers[i];
656
+ write_le64(buf + off, (uint64_t)l->capacity);
657
+ off += 8;
658
+ write_le64(buf + off, (uint64_t)l->count);
659
+ off += 8;
660
+ write_le64(buf + off, (uint64_t)l->size);
661
+ off += 8;
662
+ write_le32(buf + off, (uint32_t)l->num_hashes);
663
+ off += 4;
664
+ write_le32(buf + off, 0);
665
+ off += 4;
666
+ memcpy(buf + off, l->bits, l->size);
667
+ off += l->size;
668
+ }
669
+
670
+ return str;
671
+ }
672
+
673
+ static VALUE bloom_load(VALUE klass, VALUE data) {
674
+ Check_Type(data, T_STRING);
675
+
676
+ const uint8_t *buf = (const uint8_t *)RSTRING_PTR(data);
677
+ size_t data_len = (size_t)RSTRING_LEN(data);
678
+
679
+ if (data_len < HEADER_SIZE)
680
+ rb_raise(rb_eArgError, "data too short for bloom filter header");
681
+
682
+ size_t off = 0;
683
+ uint32_t version = read_le32(buf + off);
684
+ off += 4;
685
+ if (version != SERIAL_VERSION)
686
+ rb_raise(rb_eArgError, "unsupported serialization version: %u", version);
687
+ off += 4;
688
+
689
+ VALUE obj = bloom_alloc(klass);
690
+ ScalableBloom *sb;
691
+ TypedData_Get_Struct(obj, ScalableBloom, &scalable_bloom_type, sb);
692
+
693
+ sb->error_rate = read_le_double(buf + off);
694
+ off += 8;
695
+ sb->tightening = read_le_double(buf + off);
696
+ off += 8;
697
+ sb->initial_capacity = (size_t)read_le64(buf + off);
698
+ off += 8;
699
+ sb->total_count = (size_t)read_le64(buf + off);
700
+ off += 8;
701
+
702
+ size_t num_layers = (size_t)read_le64(buf + off);
703
+ off += 8;
704
+
705
+ if (sb->error_rate <= 0 || sb->error_rate >= 1)
706
+ rb_raise(rb_eArgError, "invalid error_rate in serialized data");
707
+ if (sb->tightening <= 0 || sb->tightening >= 1)
708
+ rb_raise(rb_eArgError, "invalid tightening in serialized data");
709
+ if (num_layers > 1000)
710
+ rb_raise(rb_eArgError, "unreasonable number of layers: %zu", num_layers);
711
+
712
+ sb->layers_cap = num_layers < 4 ? 4 : num_layers;
713
+ sb->layers = (BloomLayer **)calloc(sb->layers_cap, sizeof(BloomLayer *));
714
+ if (!sb->layers)
715
+ rb_raise(rb_eNoMemError, "failed to allocate layers array");
716
+
717
+ for (size_t i = 0; i < num_layers; i++) {
718
+ if (off + LAYER_META > data_len) {
719
+ for (size_t j = 0; j < sb->num_layers; j++)
720
+ layer_free(sb->layers[j]);
721
+ sb->num_layers = 0;
722
+ rb_raise(rb_eArgError, "data truncated at layer %zu metadata", i);
723
+ }
724
+
725
+ BloomLayer *l = (BloomLayer *)calloc(1, sizeof(BloomLayer));
726
+ if (!l) {
727
+ for (size_t j = 0; j < sb->num_layers; j++)
728
+ layer_free(sb->layers[j]);
729
+ sb->num_layers = 0;
730
+ rb_raise(rb_eNoMemError, "failed to allocate layer");
731
+ }
732
+
733
+ l->capacity = (size_t)read_le64(buf + off);
734
+ off += 8;
735
+ l->count = (size_t)read_le64(buf + off);
736
+ off += 8;
737
+ l->size = (size_t)read_le64(buf + off);
738
+ off += 8;
739
+ l->num_hashes = (int)read_le32(buf + off);
740
+ off += 4;
741
+ off += 4;
742
+
743
+ if (l->size > (1ULL << 30) || off + l->size > data_len) {
744
+ free(l);
745
+ for (size_t j = 0; j < sb->num_layers; j++)
746
+ layer_free(sb->layers[j]);
747
+ sb->num_layers = 0;
748
+ rb_raise(rb_eArgError, "invalid or truncated layer %zu", i);
749
+ }
750
+
751
+ l->bits = (uint8_t *)malloc(l->size);
752
+ if (!l->bits) {
753
+ free(l);
754
+ for (size_t j = 0; j < sb->num_layers; j++)
755
+ layer_free(sb->layers[j]);
756
+ sb->num_layers = 0;
757
+ rb_raise(rb_eNoMemError, "failed to allocate bits");
758
+ }
759
+ memcpy(l->bits, buf + off, l->size);
760
+ off += l->size;
761
+
762
+ sb->layers[sb->num_layers++] = l;
763
+ }
764
+
765
+ return obj;
766
+ }
534
767
 
535
768
  void Init_fast_bloom_filter(void) {
536
769
  VALUE mFastBloomFilter = rb_define_module("FastBloomFilter");
537
770
  VALUE cFilter = rb_define_class_under(mFastBloomFilter, "Filter", rb_cObject);
538
771
 
539
772
  rb_define_alloc_func(cFilter, bloom_alloc);
540
- rb_define_method(cFilter, "initialize", bloom_initialize, -1);
541
- rb_define_method(cFilter, "add", bloom_add, 1);
542
- rb_define_method(cFilter, "<<", bloom_add, 1);
543
- rb_define_method(cFilter, "include?", bloom_include, 1);
544
- rb_define_method(cFilter, "member?", bloom_include, 1);
545
- rb_define_method(cFilter, "clear", bloom_clear, 0);
546
- rb_define_method(cFilter, "stats", bloom_stats, 0);
547
- rb_define_method(cFilter, "count", bloom_count, 0);
548
- rb_define_method(cFilter, "size", bloom_count, 0);
549
- rb_define_method(cFilter, "num_layers", bloom_num_layers, 0);
550
- rb_define_method(cFilter, "merge!", bloom_merge, 1);
773
+ rb_define_method(cFilter, "initialize", bloom_initialize, -1);
774
+ rb_define_method(cFilter, "add", bloom_add, 1);
775
+ rb_define_method(cFilter, "<<", bloom_add, 1);
776
+ rb_define_method(cFilter, "add_if_absent", bloom_add_if_absent, 1);
777
+ rb_define_method(cFilter, "include?", bloom_include, 1);
778
+ rb_define_method(cFilter, "member?", bloom_include, 1);
779
+ rb_define_method(cFilter, "clear", bloom_clear, 0);
780
+ rb_define_method(cFilter, "stats", bloom_stats, 0);
781
+ rb_define_method(cFilter, "count", bloom_count, 0);
782
+ rb_define_method(cFilter, "size", bloom_count, 0);
783
+ rb_define_method(cFilter, "num_layers", bloom_num_layers, 0);
784
+ rb_define_method(cFilter, "merge!", bloom_merge, 1);
785
+ rb_define_method(cFilter, "dump", bloom_dump, 0);
786
+
787
+ rb_define_singleton_method(cFilter, "load", bloom_load, 1);
551
788
  }
@@ -1,3 +1,3 @@
1
1
  module FastBloomFilter
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_bloom_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-12 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -104,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
104
  - !ruby/object:Gem::Version
105
105
  version: '0'
106
106
  requirements: []
107
- rubygems_version: 3.4.22
107
+ rubygems_version: 3.3.27
108
108
  signing_key:
109
109
  specification_version: 4
110
110
  summary: Scalable Bloom Filter in C for Ruby - grows automatically