use apache_datasketches::hll::{HllSketch, TargetHllType};
use std::hint::black_box;
use std::time::{Duration, Instant};
const LG_K: u8 = 12;
const SER_CALLS: u64 = 20_000;
const DESER_CALLS: u64 = 5_000;
const HOT_KEY_SPACE: u64 = 1 << 10;
const STR_KEY_SPACE: u64 = 1 << 16;
fn string_keys() -> Vec<String> {
(0..STR_KEY_SPACE).map(|i| format!("key_{i:010}")).collect()
}
const LADDER: [u64; 3] = [1_000_000, 10_000_000, 100_000_000];
const DEFAULT_ITEMS: u64 = 10_000_000;
const DEFAULT_REPS: usize = 3;
fn parse_args() -> (Vec<u64>, usize) {
let mut items = None;
let mut reps = DEFAULT_REPS;
let mut ladder = false;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--ladder" => ladder = true,
"--reps" => {
reps = args
.next()
.and_then(|v| v.parse().ok())
.filter(|&n| n > 0)
.expect("--reps needs a positive integer")
}
other => {
let n = other
.parse()
.expect("item count must be a positive integer");
assert!(n > 0, "item count must be a positive integer");
items = Some(n);
}
}
}
assert!(
!(ladder && items.is_some()),
"pass an item count or --ladder, not both"
);
let counts = if ladder {
LADDER.to_vec()
} else {
vec![items.unwrap_or(DEFAULT_ITEMS)]
};
(counts, reps)
}
fn report(label: &str, items: u64, passes: &[Pass]) {
report_line(label, items, items, passes, String::new());
}
fn report_bytes(label: &str, items: u64, ops: u64, passes: &[Pass], bytes: usize) {
report_line(label, items, ops, passes, format!(" bytes={bytes}"));
}
fn report_line(label: &str, items: u64, ops: u64, passes: &[Pass], suffix: String) {
for (i, pass) in passes.iter().enumerate() {
assert_eq!(
pass.estimate, passes[0].estimate,
"rep {i} estimated {} but rep 0 estimated {}: the reps are not running \
the same workload",
pass.estimate, passes[0].estimate
);
}
let mut ns_per_op: Vec<f64> = passes
.iter()
.map(|p| p.elapsed.as_secs_f64() * 1e9 / ops as f64)
.collect();
ns_per_op.sort_by(f64::total_cmp);
let median = ns_per_op[(ns_per_op.len() - 1) / 2];
let (min, max) = (ns_per_op[0], ns_per_op[ns_per_op.len() - 1]);
let rate = 1000.0 / median;
let (reps, estimate) = (passes.len(), passes[0].estimate);
println!(
"{label:9} {items:>12} items {median:>7.2} ns/op min {min:>7.2} max {max:>7.2} \
{rate:>8.1} M/s reps={reps} estimate={estimate:.0}{suffix}"
);
}
struct Pass {
elapsed: Duration,
estimate: f64,
}
fn build() -> HllSketch {
HllSketch::new(LG_K, TargetHllType::Hll8).expect("fixed valid parameters were rejected")
}
fn bench_distinct(items: u64, reps: usize) {
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let mut sketch = build();
let start = Instant::now();
for key in 0..items {
sketch.update_u64(key);
}
let elapsed = start.elapsed();
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report("distinct", items, &passes);
}
fn bench_hot(items: u64, reps: usize) {
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let mut sketch = build();
let start = Instant::now();
for i in 0..items {
sketch.update_u64(i % HOT_KEY_SPACE);
}
let elapsed = start.elapsed();
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report("hot", items, &passes);
}
fn bench_str(items: u64, reps: usize) {
let keys = string_keys();
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let mut sketch = build();
let start = Instant::now();
for i in 0..items {
sketch.update_str(&keys[(i % STR_KEY_SPACE) as usize]);
}
let elapsed = start.elapsed();
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report("str", items, &passes);
}
fn bench_serde(items: u64, reps: usize) {
let mut sketch = build();
for key in 0..items {
sketch.update_u64(key);
}
let reference = sketch.serialize_compact();
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let start = Instant::now();
let mut total = 0usize;
for _ in 0..SER_CALLS {
total += black_box(sketch.serialize_compact()).len();
}
let elapsed = start.elapsed();
black_box(total);
passes.push(Pass {
elapsed,
estimate: sketch.get_estimate(),
});
}
report_bytes("ser", items, SER_CALLS, &passes, reference.len());
let deserialize = || HllSketch::deserialize(&reference).expect("the bytes came from serialize");
let mut passes = Vec::with_capacity(reps);
for _ in 0..reps {
let start = Instant::now();
let mut total = 0.0;
for _ in 0..DESER_CALLS {
total += deserialize().get_estimate();
}
let elapsed = start.elapsed();
black_box(total);
passes.push(Pass {
elapsed,
estimate: deserialize().get_estimate(),
});
}
report_bytes("deser", items, DESER_CALLS, &passes, reference.len());
}
fn main() {
let (counts, reps) = parse_args();
for items in counts {
println!("lg_config_k={LG_K} target=Hll8 items={items} reps={reps}");
bench_distinct(items, reps);
bench_hot(items, reps);
bench_str(items, reps);
bench_serde(items, reps);
}
}