Skip to content

Commit 81aa96d

Browse files
committed
Implement HyperLogLog and enable composition with Count–min sketch
1 parent 0ad73f1 commit 81aa96d

14 files changed

Lines changed: 2425 additions & 301 deletions

.rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ merge_imports = true
66
use_field_init_shorthand = true
77
use_try_shorthand = true
88
# wrap_comments = true
9+
ignore = ["src/distinct/consts.rs"]

.travis.yml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ addons:
1212

1313
env:
1414
global:
15-
- FMT_VERSION=nightly-2018-08-13
16-
- CLIPPY_VERSION=nightly-2018-08-13
15+
- FMT_VERSION=nightly-2018-09-18
16+
- CLIPPY_VERSION=nightly-2018-09-18
1717
- DOCS_RS_VERSION=nightly-2018-06-03 # https://github.com/onur/docs.rs/blob/32102ce458b34f750ef190182116d9583491a64b/Vagrantfile#L50
1818

1919
matrix:
@@ -41,6 +41,12 @@ matrix:
4141

4242
before_script: |
4343
( set -o errexit;set -o pipefail; set -o xtrace;set -o nounset;
44+
if [ "$TRAVIS_OS_NAME" = "linux" -a "${DIST-}" = "precise" ]; then
45+
# Bump libstdc++6 for libLLVM-8svn.so which requires GLIBCXX_3.4.17 https://askubuntu.com/questions/575505/glibcxx-3-4-20-not-found-how-to-fix-this-error
46+
sudo -E add-apt-repository -y ppa:ubuntu-toolchain-r/test
47+
sudo -E apt-get update
48+
sudo -E apt-get -yq --no-install-suggests --no-install-recommends $(travis_apt_get_options) install libstdc++6-4.7-dev
49+
fi
4450
for TARGET in $BUILD $RUN; do [ "$TARGET" = $(rustup target list | grep default | grep -o -e '^[^ ]\+') ] || travis_retry rustup target add "$TARGET"; done
4551
travis_retry rustup toolchain add $FMT_VERSION
4652
travis_retry rustup toolchain add $CLIPPY_VERSION
@@ -52,18 +58,18 @@ before_script: |
5258
script: |
5359
( set -o errexit;set -o pipefail; set -o xtrace;set -o nounset;
5460
cargo +$FMT_VERSION fmt -- --check
55-
cargo +$CLIPPY_VERSION clippy -- -D warnings
61+
RUSTFLAGS="-C target-cpu=native" cargo +$CLIPPY_VERSION clippy --features "simd-accel avx-accel" --lib --tests -- -D warnings
5662
OLD_IFS=$IFS IFS=";"; for RUSTFLAGS in $RUSTFLAGS_VARIATIONS; do ( IFS=$OLD_IFS
5763
export RUSTFLAGS
5864
for TARGET in $BUILD $RUN; do (
59-
[ \( "$TRAVIS_OS_NAME" = "osx" -o \( "$TRAVIS_OS_NAME" = "linux" -a "${DIST-}" = "precise" \) \) -a \( "$TARGET" = "x86_64-unknown-linux-musl" -o "$TARGET" = "i686-unknown-linux-musl" \) ] && exit 0 # RUSTFLAGS="$RUSTFLAGS -C linker=rust-lld -Z linker-flavor=ld.lld" https://github.com/rust-lang/rust/issues/52634
60-
cargo build --verbose --target "$TARGET" --lib --tests
61-
cargo build --verbose --target "$TARGET" --lib --tests --release
65+
[ \( "$TRAVIS_OS_NAME" = "osx" -o \( "$TRAVIS_OS_NAME" = "linux" -a "${DIST-}" = "precise" \) \) -a \( "$TARGET" = "x86_64-unknown-linux-musl" -o "$TARGET" = "i686-unknown-linux-musl" \) ] && RUSTFLAGS="$RUSTFLAGS -C linker=rust-lld -Z linker-flavor=ld.lld"
66+
RUSTFLAGS="$RUSTFLAGS -C target-cpu=native" cargo build --verbose --target "$TARGET" --features "simd-accel avx-accel" --lib --tests
67+
RUSTFLAGS="$RUSTFLAGS -C target-cpu=native" cargo build --verbose --target "$TARGET" --features "simd-accel avx-accel" --lib --tests --release
6268
); done
6369
for TARGET in $RUN; do (
64-
[ \( "$TRAVIS_OS_NAME" = "osx" -o \( "$TRAVIS_OS_NAME" = "linux" -a "${DIST-}" = "precise" \) \) -a \( "$TARGET" = "x86_64-unknown-linux-musl" -o "$TARGET" = "i686-unknown-linux-musl" \) ] && exit 0 # RUSTFLAGS="$RUSTFLAGS -C linker=rust-lld -Z linker-flavor=ld.lld"
65-
RUST_BACKTRACE=full cargo test --target "$TARGET"
66-
RUST_BACKTRACE=full cargo test --target "$TARGET" --release
70+
[ \( "$TRAVIS_OS_NAME" = "osx" -o \( "$TRAVIS_OS_NAME" = "linux" -a "${DIST-}" = "precise" \) \) -a \( "$TARGET" = "x86_64-unknown-linux-musl" -o "$TARGET" = "i686-unknown-linux-musl" \) ] && RUSTFLAGS="$RUSTFLAGS -C linker=rust-lld -Z linker-flavor=ld.lld"
71+
RUSTFLAGS="$RUSTFLAGS -C target-cpu=native" RUST_BACKTRACE=full cargo test --target "$TARGET" --features "simd-accel avx-accel"
72+
RUSTFLAGS="$RUSTFLAGS -C target-cpu=native" RUST_BACKTRACE=full cargo test --target "$TARGET" --features "simd-accel avx-accel" --release
6773
); done
6874
); done; IFS=$OLD_IFS
6975
for TARGET in $BUILD $RUN; do

Cargo.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ name = "streaming_algorithms"
33
version = "0.1.0"
44
license = "Apache-2.0"
55
authors = ["Alec Mocatta <alec@mocatta.net>"]
6-
categories = ["development-tools","encoding","rust-patterns","network-programming"]
7-
keywords = ["trait-object","serialization","serde","distributed"]
6+
categories = ["data-structures","algorithms","science"]
7+
keywords = ["streaming-algorithm","probabilistic","sketch","data-structure","hyperloglog"]
88
description = """
9-
Performant implementations of various streaming algorithms.
9+
Performant implementations of various streaming algorithms, including Count–min sketch; Top k; HyperLogLog; Reservoir sampling.
1010
"""
1111
repository = "https://github.com/alecmocatta/streaming_algorithms"
1212
homepage = "https://github.com/alecmocatta/streaming_algorithms"
@@ -26,3 +26,8 @@ twox-hash = "1.1"
2626
serde_derive = "1.0"
2727
serde = "1.0"
2828
rand = "0.5"
29+
bytecount = "0.3"
30+
31+
[features]
32+
avx-accel = ["bytecount/avx-accel"]
33+
simd-accel = ["bytecount/simd-accel"]

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@
1010

1111
Performant implementations of various [streaming algorithms](https://en.wikipedia.org/wiki/Streaming_algorithm).
1212

13-
This library is a work in progress. See the docs for what algorithms are currently implemented.
13+
This library is a work in progress. PRs are very welcome! Currently implemented algorithms include:
1414

15-
See [here](https://gist.github.com/debasishg/8172796) for a good list of algorithms to be implemented.
15+
* Count–min sketch
16+
* Top k (Count–min sketch plus a doubly linked hashmap to track heavy hitters / top k keys when ordered by aggregated value)
17+
* HyperLogLog
18+
* Reservoir sampling
1619

17-
As these implementations are often in hot code paths, unsafe is used, albeit only when justified.
20+
A goal of this library is to enable composition of these algorithms; for example Top k + HyperLogLog to enable roughly `SELECT key FROM table GROUP BY key ORDER BY COUNT(DISTINCT value) DESC LIMIT k`.
1821

19-
This library leverages the following prioritisation when deciding whether `unsafe` is justified for a particular implementation:
20-
1. Asymptotically optimal algorithm
21-
2. Trivial safety (i.e. no `unsafe` at all or extremely limited `unsafe` trivially contained to one or two lines)
22-
3. Constant-factor optimisations
22+
See [this gist](https://gist.github.com/debasishg/8172796) for a good list of further algorithms to be implemented. Other resources are [Probabilistic data structures – Wikipedia](https://en.wikipedia.org/wiki/Category:Probabilistic_data_structures), [DataSketches – A similar Java library originating at Yahoo](https://datasketches.github.io/), and [Algebird – A similar Java library originating at Twitter](https://github.com/twitter/algebird).
23+
24+
As these implementations are often in hot code paths, unsafe is used, albeit only when necessary to a) achieve the asymptotically optimal algorithm or b) mitigate an observed bottleneck.
2325

2426
## License
2527
Licensed under Apache License, Version 2.0, ([LICENSE.txt](LICENSE.txt) or http://www.apache.org/licenses/LICENSE-2.0).

src/count_min.rs

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// This file includes source code from https://github.com/jedisct1/rust-count-min-sketch/blob/088274e22a3decc986dec928c92cc90a709a0274/src/lib.rs under the following MIT License:
2+
3+
// Copyright (c) 2016 Frank Denis
4+
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
use serde::{de::Deserialize, ser::Serialize};
24+
use std::{
25+
borrow::Borrow, cmp::max, fmt, hash::{Hash, Hasher}, marker::PhantomData, ops
26+
};
27+
use traits::{Intersect, New, UnionAssign};
28+
use twox_hash::XxHash;
29+
30+
/// An implementation of a [count-min sketch](https://en.wikipedia.org/wiki/Count–min_sketch) data structure with *conservative updating* for increased accuracy.
31+
///
32+
/// This data structure is also known as a [counting Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter#Counting_filters).
33+
///
34+
/// See [*An Improved Data Stream Summary: The Count-Min Sketch and its Applications*](http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf) and [*New Directions in Traffic Measurement and Accounting*](http://pages.cs.wisc.edu/~suman/courses/740/papers/estan03tocs.pdf) for background on the count-min sketch with conservative updating.
35+
#[derive(Serialize, Deserialize)]
36+
#[serde(bound(
37+
serialize = "C: Serialize, <C as New>::Config: Serialize",
38+
deserialize = "C: Deserialize<'de>, <C as New>::Config: Deserialize<'de>"
39+
))]
40+
pub struct CountMinSketch<K: ?Sized, C: New> {
41+
counters: Vec<Vec<C>>,
42+
// offsets: Vec<usize>,
43+
mask: usize,
44+
k_num: usize,
45+
config: <C as New>::Config,
46+
marker: PhantomData<fn(K)>,
47+
}
48+
49+
impl<K: ?Sized, C> CountMinSketch<K, C>
50+
where
51+
K: Hash,
52+
C: New + for<'a> UnionAssign<&'a C> + Intersect,
53+
{
54+
/// Create an empty `CountMinSketch` data structure with the specified error tolerance.
55+
pub fn new(probability: f64, tolerance: f64, config: C::Config) -> Self {
56+
let width = Self::optimal_width(tolerance);
57+
let k_num = Self::optimal_k_num(probability);
58+
let counters: Vec<Vec<C>> = (0..k_num)
59+
.map(|_| (0..width).map(|_| C::new(&config)).collect())
60+
.collect();
61+
Self {
62+
counters,
63+
mask: Self::mask(width),
64+
k_num,
65+
config,
66+
marker: PhantomData,
67+
}
68+
}
69+
70+
/// "Visit" an element.
71+
pub fn push<Q: ?Sized, V: ?Sized>(&mut self, key: &Q, value: &V) -> C
72+
where
73+
Q: Hash,
74+
K: Borrow<Q>,
75+
C: for<'a> ops::AddAssign<&'a V>,
76+
{
77+
let offsets = self.offsets(key).take(self.k_num).collect::<Vec<_>>();
78+
let mut lowest = C::intersect(
79+
offsets
80+
.iter()
81+
.cloned()
82+
.enumerate()
83+
.map(|(k_i, offset)| &self.counters[k_i][offset]),
84+
).unwrap();
85+
lowest += value;
86+
for (k_i, offset) in offsets.into_iter().enumerate() {
87+
self.counters[k_i][offset].union_assign(&lowest);
88+
}
89+
lowest
90+
}
91+
92+
/// Union the aggregated value for `key` with `value`.
93+
pub fn union_assign<Q: ?Sized>(&mut self, key: &Q, value: &C)
94+
where
95+
Q: Hash,
96+
K: Borrow<Q>,
97+
{
98+
for (k_i, offset) in self.offsets(key).take(self.k_num).enumerate() {
99+
self.counters[k_i][offset].union_assign(value);
100+
}
101+
}
102+
103+
/// Retrieve an estimate of the aggregated value for `key`.
104+
pub fn get<Q: ?Sized>(&self, key: &Q) -> C
105+
where
106+
Q: Hash,
107+
K: Borrow<Q>,
108+
{
109+
C::intersect(
110+
self.offsets(key)
111+
.take(self.k_num)
112+
.enumerate()
113+
.map(|(k_i, offset)| &self.counters[k_i][offset]),
114+
).unwrap()
115+
}
116+
117+
// pub fn estimate_memory(
118+
// probability: f64, tolerance: f64,
119+
// ) -> Result<usize, &'static str> {
120+
// let width = Self::optimal_width(tolerance);
121+
// let k_num = Self::optimal_k_num(probability);
122+
// Ok(width * mem::size_of::<C>() * k_num)
123+
// }
124+
125+
/// Clears the `CountMinSketch` data structure, as if it was new.
126+
pub fn clear(&mut self) {
127+
for k_i in 0..self.k_num {
128+
for counter in &mut self.counters[k_i] {
129+
*counter = C::new(&self.config);
130+
}
131+
}
132+
}
133+
134+
fn optimal_width(tolerance: f64) -> usize {
135+
let e = tolerance;
136+
let width = (2.0 / e).round() as usize;
137+
max(2, width)
138+
.checked_next_power_of_two()
139+
.expect("Width would be way too large")
140+
}
141+
142+
fn mask(width: usize) -> usize {
143+
assert!(width > 1);
144+
assert_eq!(width & (width - 1), 0);
145+
width - 1
146+
}
147+
148+
fn optimal_k_num(probability: f64) -> usize {
149+
max(1, ((1.0 - probability).ln() / 0.5_f64.ln()) as usize)
150+
}
151+
152+
fn offsets<Q: ?Sized>(&self, key: &Q) -> impl Iterator<Item = usize>
153+
where
154+
Q: Hash,
155+
K: Borrow<Q>,
156+
{
157+
// if k_i < 2 {
158+
// let sip = &mut self.hashers[k_i as usize].clone();
159+
// key.hash(sip);
160+
// let hash = sip.finish();
161+
// hashes[k_i as usize] = hash;
162+
// hash as usize & self.mask
163+
// } else {
164+
// hashes[0]
165+
// .wrapping_add((k_i as u64).wrapping_mul(hashes[1]) %
166+
// 0xffffffffffffffc5) as usize & self.mask
167+
// }
168+
let mask = self.mask;
169+
hashes(key).map(move |hash| hash as usize & mask)
170+
}
171+
}
172+
173+
fn hashes<Q: ?Sized>(key: &Q) -> impl Iterator<Item = u64>
174+
where
175+
Q: Hash,
176+
{
177+
#[allow(missing_copy_implementations, missing_debug_implementations)]
178+
struct X(XxHash);
179+
impl Iterator for X {
180+
type Item = u64;
181+
fn next(&mut self) -> Option<Self::Item> {
182+
let ret = self.0.finish();
183+
self.0.write(&[123]);
184+
Some(ret)
185+
}
186+
}
187+
let mut hasher = XxHash::default();
188+
key.hash(&mut hasher);
189+
X(hasher)
190+
}
191+
192+
impl<K: ?Sized, C: New + Clone> Clone for CountMinSketch<K, C> {
193+
fn clone(&self) -> Self {
194+
Self {
195+
counters: self.counters.clone(),
196+
mask: self.mask,
197+
k_num: self.k_num,
198+
config: self.config.clone(),
199+
marker: PhantomData,
200+
}
201+
}
202+
}
203+
impl<K: ?Sized, C: New> fmt::Debug for CountMinSketch<K, C> {
204+
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
205+
fmt.debug_struct("CountMinSketch")
206+
// .field("counters", &self.counters)
207+
.finish()
208+
}
209+
}
210+
211+
#[cfg(test)]
212+
mod tests {
213+
type CountMinSketch8<K> = super::CountMinSketch<K, u8>;
214+
type CountMinSketch16<K> = super::CountMinSketch<K, u16>;
215+
type CountMinSketch64<K> = super::CountMinSketch<K, u64>;
216+
217+
#[ignore] // release mode stops panic
218+
#[test]
219+
#[should_panic]
220+
fn test_overflow() {
221+
let mut cms = CountMinSketch8::<&str>::new(0.95, 10.0 / 100.0, ());
222+
for _ in 0..300 {
223+
let _ = cms.push("key", &1);
224+
}
225+
// assert_eq!(cms.get("key"), &u8::max_value());
226+
}
227+
228+
#[test]
229+
fn test_increment() {
230+
let mut cms = CountMinSketch16::<&str>::new(0.95, 10.0 / 100.0, ());
231+
for _ in 0..300 {
232+
let _ = cms.push("key", &1);
233+
}
234+
assert_eq!(cms.get("key"), 300);
235+
}
236+
237+
#[test]
238+
fn test_increment_multi() {
239+
let mut cms = CountMinSketch64::<u64>::new(0.99, 2.0 / 100.0, ());
240+
for i in 0..1_000_000 {
241+
let _ = cms.push(&(i % 100), &1);
242+
}
243+
for key in 0..100 {
244+
assert!(cms.get(&key) >= 9_000);
245+
}
246+
// cms.reset();
247+
// for key in 0..100 {
248+
// assert!(cms.get(&key) < 11_000);
249+
// }
250+
}
251+
}

0 commit comments

Comments
 (0)