probabilistic_machines 0.1.1
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 +7 -0
- data/CHANGELOG.md +58 -0
- data/Cargo.lock +294 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/ext/probabilistic_machines_native/core/Cargo.toml +24 -0
- data/ext/probabilistic_machines_native/core/src/countmin.rs +240 -0
- data/ext/probabilistic_machines_native/core/src/cuckoo.rs +283 -0
- data/ext/probabilistic_machines_native/core/src/hash.rs +88 -0
- data/ext/probabilistic_machines_native/core/src/hyperloglog.rs +254 -0
- data/ext/probabilistic_machines_native/core/src/lib.rs +65 -0
- data/ext/probabilistic_machines_native/core/src/tdigest.rs +445 -0
- data/ext/probabilistic_machines_native/core/src/topk.rs +325 -0
- data/ext/probabilistic_machines_native/extconf.rb +3 -0
- data/ext/probabilistic_machines_native/ffi/Cargo.toml +20 -0
- data/ext/probabilistic_machines_native/ffi/extconf.rb +21 -0
- data/ext/probabilistic_machines_native/ffi/src/countmin.rs +188 -0
- data/ext/probabilistic_machines_native/ffi/src/cuckoo.rs +149 -0
- data/ext/probabilistic_machines_native/ffi/src/data.rs +69 -0
- data/ext/probabilistic_machines_native/ffi/src/hyperloglog.rs +155 -0
- data/ext/probabilistic_machines_native/ffi/src/lib.rs +140 -0
- data/ext/probabilistic_machines_native/ffi/src/tdigest.rs +166 -0
- data/ext/probabilistic_machines_native/ffi/src/topk.rs +299 -0
- data/lib/probabilistic_machines/version.rb +5 -0
- data/lib/probabilistic_machines.rb +7 -0
- data/sig/probabilistic_machines.rbs +182 -0
- metadata +90 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//! Frequency estimation for a stream nobody wants to store.
|
|
2
|
+
|
|
3
|
+
use crate::hash::{hash64, mix64, u32_low_u8, u64_low_u16, usize_low_u8};
|
|
4
|
+
use crate::{CodecError, TAG_COUNTMIN, read8};
|
|
5
|
+
|
|
6
|
+
const HEADER_LEN: usize = 12;
|
|
7
|
+
const COUNTER_LEN: usize = 4;
|
|
8
|
+
|
|
9
|
+
/// Counts how often each item appeared, in `W * D` counters.
|
|
10
|
+
///
|
|
11
|
+
/// `W` is the row width, a power of two up to 65536; `D` is the number of rows.
|
|
12
|
+
/// An estimate is never below the true count while [`saturated`](Self::saturated)
|
|
13
|
+
/// is false, and overshoots it by more than
|
|
14
|
+
/// `e * total / W` with probability at most `e^-D`, where `total` is the sum of
|
|
15
|
+
/// every count added. 2048 columns over 5 rows hold the overshoot under 0.14%
|
|
16
|
+
/// of the total with probability 0.993.
|
|
17
|
+
///
|
|
18
|
+
/// ```
|
|
19
|
+
/// use probabilistic_machines::CountMinSketch;
|
|
20
|
+
///
|
|
21
|
+
/// let mut hits = CountMinSketch::<1024, 4>::new();
|
|
22
|
+
/// hits.add_count(b"/login", 900);
|
|
23
|
+
/// hits.add(b"/health");
|
|
24
|
+
///
|
|
25
|
+
/// assert!(hits.estimate(b"/login") >= 900);
|
|
26
|
+
/// assert_eq!(hits.estimate(b"/never-seen"), 0);
|
|
27
|
+
/// assert_eq!(hits.total(), 901);
|
|
28
|
+
/// ```
|
|
29
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
30
|
+
pub struct CountMinSketch<const W: usize, const D: usize> {
|
|
31
|
+
rows: [[u32; W]; D],
|
|
32
|
+
saturated: bool,
|
|
33
|
+
total: u64,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl<const W: usize, const D: usize> CountMinSketch<W, D> {
|
|
37
|
+
const COLUMN_MASK: usize = {
|
|
38
|
+
assert!(W.is_power_of_two(), "row width must be a power of two");
|
|
39
|
+
assert!(W >= 2, "row width must be at least 2");
|
|
40
|
+
assert!(W <= 1 << 16, "row width must be at most 65536");
|
|
41
|
+
assert!(D >= 1, "a sketch needs at least one row");
|
|
42
|
+
assert!(D <= 64, "a sketch needs at most 64 rows");
|
|
43
|
+
W - 1
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/// Length of the [`encode`](Self::encode) frame.
|
|
47
|
+
pub const ENCODED_LEN: usize = HEADER_LEN + W * D * COUNTER_LEN;
|
|
48
|
+
|
|
49
|
+
#[must_use]
|
|
50
|
+
pub const fn new() -> Self {
|
|
51
|
+
Self {
|
|
52
|
+
rows: [[0; W]; D],
|
|
53
|
+
total: 0,
|
|
54
|
+
saturated: false,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Sum of every count added, saturating at [`u64::MAX`].
|
|
59
|
+
#[must_use]
|
|
60
|
+
pub const fn total(&self) -> u64 {
|
|
61
|
+
self.total
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// True once a counter has hit [`u32::MAX`], after which an estimate can
|
|
65
|
+
/// fall below the true count.
|
|
66
|
+
#[must_use]
|
|
67
|
+
pub const fn saturated(&self) -> bool {
|
|
68
|
+
self.saturated
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn add(&mut self, item: &[u8]) {
|
|
72
|
+
self.add_count(item, 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn add_count(&mut self, item: &[u8], count: u32) {
|
|
76
|
+
self.add_hash_count(hash64(item), count);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Adds to an item the caller already hashed.
|
|
80
|
+
pub fn add_hash_count(&mut self, hash: u64, count: u32) {
|
|
81
|
+
let mut cursor = hash;
|
|
82
|
+
for row in &mut self.rows {
|
|
83
|
+
cursor = mix64(cursor);
|
|
84
|
+
let column = usize::from(u64_low_u16(cursor)) & Self::COLUMN_MASK;
|
|
85
|
+
let (sum, overflowed) = row[column].overflowing_add(count);
|
|
86
|
+
self.saturated |= overflowed;
|
|
87
|
+
row[column] = if overflowed { u32::MAX } else { sum };
|
|
88
|
+
}
|
|
89
|
+
self.total = self.total.saturating_add(u64::from(count));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Upper bound on how often `item` was added.
|
|
93
|
+
#[must_use]
|
|
94
|
+
pub fn estimate(&self, item: &[u8]) -> u32 {
|
|
95
|
+
self.estimate_hash(hash64(item))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Upper bound for an item the caller already hashed.
|
|
99
|
+
#[must_use]
|
|
100
|
+
pub fn estimate_hash(&self, hash: u64) -> u32 {
|
|
101
|
+
let mut cursor = hash;
|
|
102
|
+
let mut smallest = u32::MAX;
|
|
103
|
+
for row in &self.rows {
|
|
104
|
+
cursor = mix64(cursor);
|
|
105
|
+
let column = usize::from(u64_low_u16(cursor)) & Self::COLUMN_MASK;
|
|
106
|
+
if row[column] < smallest {
|
|
107
|
+
smallest = row[column];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
smallest
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Adds another sketch's counters to this one, which is exact for sketches
|
|
114
|
+
/// of the same shape: the result equals one sketch fed both streams.
|
|
115
|
+
pub fn merge(&mut self, other: &Self) {
|
|
116
|
+
let mut saturated = self.saturated || other.saturated;
|
|
117
|
+
for (mine, theirs) in self.rows.iter_mut().zip(&other.rows) {
|
|
118
|
+
for (counter, addend) in mine.iter_mut().zip(theirs) {
|
|
119
|
+
let (sum, overflowed) = counter.overflowing_add(*addend);
|
|
120
|
+
saturated |= overflowed;
|
|
121
|
+
*counter = if overflowed { u32::MAX } else { sum };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
self.saturated = saturated;
|
|
125
|
+
self.total = self.total.saturating_add(other.total);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Writes the sketch into `out`, returning the number of bytes written.
|
|
129
|
+
///
|
|
130
|
+
/// # Errors
|
|
131
|
+
///
|
|
132
|
+
/// [`CodecError::Truncated`] when `out` is shorter than
|
|
133
|
+
/// [`ENCODED_LEN`](Self::ENCODED_LEN).
|
|
134
|
+
pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
|
|
135
|
+
let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
|
|
136
|
+
return Err(CodecError::Truncated);
|
|
137
|
+
};
|
|
138
|
+
self.write_frame(frame);
|
|
139
|
+
Ok(Self::ENCODED_LEN)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// The sketch's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
143
|
+
#[cfg(feature = "alloc")]
|
|
144
|
+
#[must_use]
|
|
145
|
+
pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
|
|
146
|
+
let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
|
|
147
|
+
self.write_frame(&mut frame);
|
|
148
|
+
frame
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
152
|
+
fn write_frame(&self, frame: &mut [u8]) {
|
|
153
|
+
frame[0] = TAG_COUNTMIN;
|
|
154
|
+
frame[1] = u32_low_u8(W.trailing_zeros());
|
|
155
|
+
frame[2] = usize_low_u8(D);
|
|
156
|
+
frame[3..11].copy_from_slice(&self.total.to_le_bytes());
|
|
157
|
+
frame[11] = u8::from(self.saturated);
|
|
158
|
+
|
|
159
|
+
let counters = self.rows.iter().flatten();
|
|
160
|
+
let (slots, _) = frame[HEADER_LEN..].as_chunks_mut::<COUNTER_LEN>();
|
|
161
|
+
for (slot, counter) in slots.iter_mut().zip(counters) {
|
|
162
|
+
*slot = counter.to_le_bytes();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Reads a sketch written by [`encode`](Self::encode).
|
|
167
|
+
///
|
|
168
|
+
/// # Errors
|
|
169
|
+
///
|
|
170
|
+
/// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
|
|
171
|
+
/// when it holds another structure, [`CodecError::Parameters`] when its
|
|
172
|
+
/// shape differs from `W` by `D` or its counters contradict its total.
|
|
173
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
|
|
174
|
+
let mut sketch = Self::new();
|
|
175
|
+
sketch.decode_into(bytes)?;
|
|
176
|
+
Ok(sketch)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Reads a frame over this sketch, for a caller that owns one already and
|
|
180
|
+
/// has no room on its stack for a second.
|
|
181
|
+
///
|
|
182
|
+
/// The sketch is untouched when the frame is rejected.
|
|
183
|
+
///
|
|
184
|
+
/// # Errors
|
|
185
|
+
///
|
|
186
|
+
/// The same as [`decode`](Self::decode).
|
|
187
|
+
pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
|
|
188
|
+
let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
|
|
189
|
+
return Err(CodecError::Truncated);
|
|
190
|
+
};
|
|
191
|
+
if frame[0] != TAG_COUNTMIN {
|
|
192
|
+
return Err(CodecError::Tag);
|
|
193
|
+
}
|
|
194
|
+
if frame[1] != u32_low_u8(W.trailing_zeros()) || frame[2] != usize_low_u8(D) {
|
|
195
|
+
return Err(CodecError::Parameters);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let total = u64::from_le_bytes(read8(&frame[3..11]));
|
|
199
|
+
let saturated = frame[11] != 0;
|
|
200
|
+
let (slots, _) = frame[HEADER_LEN..].as_chunks::<COUNTER_LEN>();
|
|
201
|
+
if !Self::rows_are_coherent(slots, total, saturated) {
|
|
202
|
+
return Err(CodecError::Parameters);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
self.total = total;
|
|
206
|
+
self.saturated = saturated;
|
|
207
|
+
let counters = self.rows.iter_mut().flatten();
|
|
208
|
+
for (slot, counter) in slots.iter().zip(counters) {
|
|
209
|
+
*counter = u32::from_le_bytes(*slot);
|
|
210
|
+
}
|
|
211
|
+
Ok(())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Whether the counters carry the total the frame claims, as every sketch
|
|
215
|
+
/// built by adding items does: each row takes every count added, so it sums
|
|
216
|
+
/// to the total exactly. Only a counter stopped at [`u32::MAX`] can leave a
|
|
217
|
+
/// row short of it, and such a sketch says so in its saturation flag.
|
|
218
|
+
fn rows_are_coherent(slots: &[[u8; COUNTER_LEN]], total: u64, saturated: bool) -> bool {
|
|
219
|
+
slots.chunks(W).all(|row| {
|
|
220
|
+
let counted: u64 = row
|
|
221
|
+
.iter()
|
|
222
|
+
.map(|slot| u64::from(u32::from_le_bytes(*slot)))
|
|
223
|
+
.sum();
|
|
224
|
+
if saturated {
|
|
225
|
+
counted <= total
|
|
226
|
+
} else {
|
|
227
|
+
counted == total
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
impl<const W: usize, const D: usize> Default for CountMinSketch<W, D> {
|
|
234
|
+
fn default() -> Self {
|
|
235
|
+
Self::new()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[cfg(test)]
|
|
240
|
+
mod tests;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
//! Approximate membership with deletions, which a Bloom filter cannot do.
|
|
2
|
+
|
|
3
|
+
use crate::hash::{hash64, mix64, u32_low_u8, u64_low_u8, u64_low_u16};
|
|
4
|
+
use crate::{CodecError, TAG_CUCKOO};
|
|
5
|
+
|
|
6
|
+
const SLOTS: usize = 4;
|
|
7
|
+
const EMPTY: u8 = 0;
|
|
8
|
+
const MAX_KICKS: usize = 64;
|
|
9
|
+
|
|
10
|
+
/// The filter has no room left for this item.
|
|
11
|
+
///
|
|
12
|
+
/// Cuckoo hashing gives up before the table is literally full; expect that
|
|
13
|
+
/// past about 95% of [`CuckooFilter::CAPACITY`]. The filter is unchanged when
|
|
14
|
+
/// an insert fails.
|
|
15
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
16
|
+
pub struct Full;
|
|
17
|
+
|
|
18
|
+
impl core::fmt::Display for Full {
|
|
19
|
+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
20
|
+
f.write_str("cuckoo filter full")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl core::error::Error for Full {}
|
|
25
|
+
|
|
26
|
+
/// Answers "have I seen this?" in `B * 4` bytes, and unlike a Bloom filter it
|
|
27
|
+
/// can forget.
|
|
28
|
+
///
|
|
29
|
+
/// `B` is the bucket count, a power of two up to 65536; each bucket holds four
|
|
30
|
+
/// 8-bit fingerprints, so the filter stores up to `4 * B` items in `4 * B`
|
|
31
|
+
/// bytes. [`contains`](Self::contains) never says no to an item that was
|
|
32
|
+
/// inserted; it says yes to an item that was not with probability at most
|
|
33
|
+
/// `2 * 4 / 255`, about 3%.
|
|
34
|
+
///
|
|
35
|
+
/// [`remove`](Self::remove) must only be called for items that were inserted:
|
|
36
|
+
/// deleting an item that merely collides with one takes the other item's
|
|
37
|
+
/// fingerprint with it.
|
|
38
|
+
///
|
|
39
|
+
/// ```
|
|
40
|
+
/// use probabilistic_machines::CuckooFilter;
|
|
41
|
+
///
|
|
42
|
+
/// let mut seen = CuckooFilter::<1024>::new();
|
|
43
|
+
/// assert_eq!(seen.insert(b"device-9"), Ok(()));
|
|
44
|
+
/// assert!(seen.contains(b"device-9"));
|
|
45
|
+
///
|
|
46
|
+
/// assert!(seen.remove(b"device-9"));
|
|
47
|
+
/// assert!(!seen.contains(b"device-9"));
|
|
48
|
+
/// ```
|
|
49
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
50
|
+
pub struct CuckooFilter<const B: usize> {
|
|
51
|
+
buckets: [[u8; SLOTS]; B],
|
|
52
|
+
items: usize,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl<const B: usize> CuckooFilter<B> {
|
|
56
|
+
const BUCKET_MASK: usize = {
|
|
57
|
+
assert!(B.is_power_of_two(), "bucket count must be a power of two");
|
|
58
|
+
assert!(B >= 2, "bucket count must be at least 2");
|
|
59
|
+
assert!(B <= 1 << 16, "bucket count must be at most 65536");
|
|
60
|
+
B - 1
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/// Fingerprint slots in the table.
|
|
64
|
+
pub const CAPACITY: usize = B * SLOTS;
|
|
65
|
+
|
|
66
|
+
/// Length of the [`encode`](Self::encode) frame.
|
|
67
|
+
pub const ENCODED_LEN: usize = 2 + B * SLOTS;
|
|
68
|
+
|
|
69
|
+
#[must_use]
|
|
70
|
+
pub const fn new() -> Self {
|
|
71
|
+
Self {
|
|
72
|
+
buckets: [[EMPTY; SLOTS]; B],
|
|
73
|
+
items: 0,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Items currently stored.
|
|
78
|
+
#[must_use]
|
|
79
|
+
pub const fn len(&self) -> usize {
|
|
80
|
+
self.items
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[must_use]
|
|
84
|
+
pub const fn is_empty(&self) -> bool {
|
|
85
|
+
self.items == 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// # Errors
|
|
89
|
+
///
|
|
90
|
+
/// [`Full`] when the item's two buckets and 64 evictions all failed to
|
|
91
|
+
/// find a slot. The filter is left as it was.
|
|
92
|
+
pub fn insert(&mut self, item: &[u8]) -> Result<(), Full> {
|
|
93
|
+
self.insert_hash(hash64(item))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Inserts an item the caller already hashed.
|
|
97
|
+
///
|
|
98
|
+
/// # Errors
|
|
99
|
+
///
|
|
100
|
+
/// [`Full`] when the item's two buckets and 64 evictions all failed to
|
|
101
|
+
/// find a slot. The filter is left as it was.
|
|
102
|
+
pub fn insert_hash(&mut self, hash: u64) -> Result<(), Full> {
|
|
103
|
+
let fingerprint = fingerprint(hash);
|
|
104
|
+
let primary = Self::bucket(hash);
|
|
105
|
+
if self.place(primary, fingerprint) {
|
|
106
|
+
return Ok(());
|
|
107
|
+
}
|
|
108
|
+
let alternate = Self::alternate(primary, fingerprint);
|
|
109
|
+
if self.place(alternate, fingerprint) {
|
|
110
|
+
return Ok(());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let mut path = [0_usize; MAX_KICKS];
|
|
114
|
+
let mut index = alternate;
|
|
115
|
+
let mut carried = fingerprint;
|
|
116
|
+
let mut walk = mix64(hash);
|
|
117
|
+
|
|
118
|
+
for step in &mut path {
|
|
119
|
+
walk = mix64(walk);
|
|
120
|
+
let slot = usize::from(u64_low_u16(walk)) % SLOTS;
|
|
121
|
+
core::mem::swap(&mut carried, &mut self.buckets[index][slot]);
|
|
122
|
+
*step = index * SLOTS + slot;
|
|
123
|
+
index = Self::alternate(index, carried);
|
|
124
|
+
if self.place(index, carried) {
|
|
125
|
+
return Ok(());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for step in path.iter().rev() {
|
|
130
|
+
core::mem::swap(&mut carried, &mut self.buckets[step / SLOTS][step % SLOTS]);
|
|
131
|
+
}
|
|
132
|
+
Err(Full)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[must_use]
|
|
136
|
+
pub fn contains(&self, item: &[u8]) -> bool {
|
|
137
|
+
self.contains_hash(hash64(item))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Queries an item the caller already hashed.
|
|
141
|
+
#[must_use]
|
|
142
|
+
pub fn contains_hash(&self, hash: u64) -> bool {
|
|
143
|
+
let fingerprint = fingerprint(hash);
|
|
144
|
+
let primary = Self::bucket(hash);
|
|
145
|
+
let alternate = Self::alternate(primary, fingerprint);
|
|
146
|
+
self.buckets[primary].contains(&fingerprint)
|
|
147
|
+
|| self.buckets[alternate].contains(&fingerprint)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Drops one copy of an item, reporting whether a fingerprint was cleared.
|
|
151
|
+
pub fn remove(&mut self, item: &[u8]) -> bool {
|
|
152
|
+
self.remove_hash(hash64(item))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Drops an item the caller already hashed.
|
|
156
|
+
pub fn remove_hash(&mut self, hash: u64) -> bool {
|
|
157
|
+
let fingerprint = fingerprint(hash);
|
|
158
|
+
let primary = Self::bucket(hash);
|
|
159
|
+
let alternate = Self::alternate(primary, fingerprint);
|
|
160
|
+
self.clear(primary, fingerprint) || self.clear(alternate, fingerprint)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Writes the filter into `out`, returning the number of bytes written.
|
|
164
|
+
///
|
|
165
|
+
/// # Errors
|
|
166
|
+
///
|
|
167
|
+
/// [`CodecError::Truncated`] when `out` is shorter than
|
|
168
|
+
/// [`ENCODED_LEN`](Self::ENCODED_LEN).
|
|
169
|
+
pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
|
|
170
|
+
let Some(frame) = out.get_mut(..Self::ENCODED_LEN) else {
|
|
171
|
+
return Err(CodecError::Truncated);
|
|
172
|
+
};
|
|
173
|
+
self.write_frame(frame);
|
|
174
|
+
Ok(Self::ENCODED_LEN)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// The filter's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
178
|
+
#[cfg(feature = "alloc")]
|
|
179
|
+
#[must_use]
|
|
180
|
+
pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
|
|
181
|
+
let mut frame = alloc::vec![0_u8; Self::ENCODED_LEN].into_boxed_slice();
|
|
182
|
+
self.write_frame(&mut frame);
|
|
183
|
+
frame
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
|
|
187
|
+
fn write_frame(&self, frame: &mut [u8]) {
|
|
188
|
+
frame[0] = TAG_CUCKOO;
|
|
189
|
+
frame[1] = u32_low_u8(B.trailing_zeros());
|
|
190
|
+
|
|
191
|
+
let fingerprints = self.buckets.iter().flatten();
|
|
192
|
+
for (slot, value) in frame[2..].iter_mut().zip(fingerprints) {
|
|
193
|
+
*slot = *value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Reads a filter written by [`encode`](Self::encode).
|
|
198
|
+
///
|
|
199
|
+
/// # Errors
|
|
200
|
+
///
|
|
201
|
+
/// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
|
|
202
|
+
/// when it holds another structure, [`CodecError::Parameters`] when it was
|
|
203
|
+
/// written with a different bucket count.
|
|
204
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
|
|
205
|
+
let mut filter = Self::new();
|
|
206
|
+
filter.decode_into(bytes)?;
|
|
207
|
+
Ok(filter)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Reads a frame over this filter, for a caller that owns one already and
|
|
211
|
+
/// has no room on its stack for a second.
|
|
212
|
+
///
|
|
213
|
+
/// The filter is untouched when the frame is rejected.
|
|
214
|
+
///
|
|
215
|
+
/// # Errors
|
|
216
|
+
///
|
|
217
|
+
/// The same as [`decode`](Self::decode).
|
|
218
|
+
pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
|
|
219
|
+
let Some(frame) = bytes.get(..Self::ENCODED_LEN) else {
|
|
220
|
+
return Err(CodecError::Truncated);
|
|
221
|
+
};
|
|
222
|
+
if frame[0] != TAG_CUCKOO {
|
|
223
|
+
return Err(CodecError::Tag);
|
|
224
|
+
}
|
|
225
|
+
if frame[1] != u32_low_u8(B.trailing_zeros()) {
|
|
226
|
+
return Err(CodecError::Parameters);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
self.items = 0;
|
|
230
|
+
let fingerprints = self.buckets.iter_mut().flatten();
|
|
231
|
+
for (value, byte) in fingerprints.zip(&frame[2..]) {
|
|
232
|
+
*value = *byte;
|
|
233
|
+
if *byte != EMPTY {
|
|
234
|
+
self.items += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
Ok(())
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn bucket(hash: u64) -> usize {
|
|
241
|
+
usize::from(u64_low_u16(hash)) & Self::BUCKET_MASK
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn alternate(index: usize, fingerprint: u8) -> usize {
|
|
245
|
+
let twist = usize::from(u64_low_u16(mix64(u64::from(fingerprint)))) & Self::BUCKET_MASK;
|
|
246
|
+
index ^ twist
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn place(&mut self, index: usize, fingerprint: u8) -> bool {
|
|
250
|
+
for slot in &mut self.buckets[index] {
|
|
251
|
+
if *slot == EMPTY {
|
|
252
|
+
*slot = fingerprint;
|
|
253
|
+
self.items += 1;
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
false
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn clear(&mut self, index: usize, fingerprint: u8) -> bool {
|
|
261
|
+
for slot in &mut self.buckets[index] {
|
|
262
|
+
if *slot == fingerprint {
|
|
263
|
+
*slot = EMPTY;
|
|
264
|
+
self.items -= 1;
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
false
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
impl<const B: usize> Default for CuckooFilter<B> {
|
|
273
|
+
fn default() -> Self {
|
|
274
|
+
Self::new()
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn fingerprint(hash: u64) -> u8 {
|
|
279
|
+
u64_low_u8(mix64(hash)).max(1)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[cfg(test)]
|
|
283
|
+
mod tests;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//! The crate's only hash. Sketches index by bit slices of a 64-bit value, so
|
|
2
|
+
//! they need avalanche behaviour, not cryptography.
|
|
3
|
+
|
|
4
|
+
const PRIME: u64 = 0x9E37_79B9_7F4A_7C15;
|
|
5
|
+
|
|
6
|
+
/// `splitmix64`'s finalizer: a bijection on [`u64`].
|
|
7
|
+
///
|
|
8
|
+
/// Every output bit depends on every input bit, and iterating it yields
|
|
9
|
+
/// decorrelated values, which is how the sketches derive one index per row
|
|
10
|
+
/// from a single hash.
|
|
11
|
+
#[must_use]
|
|
12
|
+
pub const fn mix64(value: u64) -> u64 {
|
|
13
|
+
let mut z = value.wrapping_add(PRIME);
|
|
14
|
+
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
|
15
|
+
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
|
16
|
+
z ^ (z >> 31)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Hashes bytes with the crate's default seed.
|
|
20
|
+
#[must_use]
|
|
21
|
+
pub const fn hash64(bytes: &[u8]) -> u64 {
|
|
22
|
+
hash64_with_seed(bytes, 0)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Hashes bytes, folding eight at a time through [`mix64`].
|
|
26
|
+
///
|
|
27
|
+
/// Two seeds give two independent hash families, which is what a filter needs
|
|
28
|
+
/// when it wants fingerprints uncorrelated with bucket indices.
|
|
29
|
+
#[must_use]
|
|
30
|
+
pub const fn hash64_with_seed(bytes: &[u8], seed: u64) -> u64 {
|
|
31
|
+
let mut accumulator = seed ^ PRIME;
|
|
32
|
+
let mut consumed: u64 = 0;
|
|
33
|
+
let mut cursor = 0;
|
|
34
|
+
|
|
35
|
+
while cursor + 8 <= bytes.len() {
|
|
36
|
+
let lane = u64::from_le_bytes([
|
|
37
|
+
bytes[cursor],
|
|
38
|
+
bytes[cursor + 1],
|
|
39
|
+
bytes[cursor + 2],
|
|
40
|
+
bytes[cursor + 3],
|
|
41
|
+
bytes[cursor + 4],
|
|
42
|
+
bytes[cursor + 5],
|
|
43
|
+
bytes[cursor + 6],
|
|
44
|
+
bytes[cursor + 7],
|
|
45
|
+
]);
|
|
46
|
+
accumulator = (accumulator ^ mix64(lane))
|
|
47
|
+
.rotate_left(27)
|
|
48
|
+
.wrapping_mul(PRIME);
|
|
49
|
+
cursor += 8;
|
|
50
|
+
consumed += 8;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let mut tail: u64 = 0;
|
|
54
|
+
let mut shift = 0;
|
|
55
|
+
while cursor < bytes.len() {
|
|
56
|
+
tail |= (bytes[cursor] as u64) << shift;
|
|
57
|
+
shift += 8;
|
|
58
|
+
cursor += 1;
|
|
59
|
+
consumed += 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
mix64(mix64(accumulator ^ consumed.wrapping_mul(PRIME)) ^ mix64(tail))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub(crate) const fn u64_low_u16(value: u64) -> u16 {
|
|
66
|
+
let [low, high, ..] = value.to_le_bytes();
|
|
67
|
+
u16::from_le_bytes([low, high])
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub(crate) const fn u64_low_u8(value: u64) -> u8 {
|
|
71
|
+
value.to_le_bytes()[0]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub(crate) const fn u32_low_u8(value: u32) -> u8 {
|
|
75
|
+
value.to_le_bytes()[0]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub(crate) const fn usize_low_u8(value: usize) -> u8 {
|
|
79
|
+
value.to_le_bytes()[0]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub(crate) const fn usize_low_u16(value: usize) -> u16 {
|
|
83
|
+
let [low, high, ..] = value.to_le_bytes();
|
|
84
|
+
u16::from_le_bytes([low, high])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[cfg(test)]
|
|
88
|
+
mod tests;
|