Skip to main content

bench_tuple_generic_update/
bench_tuple_generic_update.rs

1//! Throughput harness for `TupleSketch::update_*` — the generic (type-erased)
2//! Tuple path, where C++ calls back into Rust to clone and combine summaries.
3//!
4//! Separate from `bench_tuple_update.rs` because the two have genuinely
5//! different cost structures. ArrayOfDoubles binds a concrete C++
6//! instantiation and its summary is a plain `f64` array; the generic path
7//! wraps every summary in a `rust::Box<RustSummary>` and reaches Rust through
8//! a trampoline for each clone and combine. There is no native C++ reference
9//! for this one — the callback design has no C++ equivalent to compare
10//! against, so the number to watch is this file against itself across changes.
11//!
12//! Run with (release matters — a debug build measures nothing useful):
13//!   cargo run --release --example bench_tuple_generic_update --features tuple
14//!   cargo run --release --example bench_tuple_generic_update --features tuple -- 100000000
15//!
16//! Fixed parameters so runs are comparable: `lg_k = 12`, defaults elsewhere,
17//! item count defaults to 10M and can be overridden as the first argument.
18//!
19//! Same two scenarios as the ArrayOfDoubles harness, for the same reason —
20//! they exercise different halves of upstream's `update_tuple_sketch::update`:
21//!
22//! - `distinct` — every key is new, so once theta drops most keys are rejected
23//!   by `hash_and_screen`, which returns *before* the update value is read.
24//!   Per-call work performed ahead of that screen is pure waste here.
25//! - `hot` — keys drawn from a space small enough to stay fully retained, so
26//!   every call reaches `union_combine`.
27//!
28//! The summary below is deliberately the cheapest possible: a single `u64`,
29//! with `create`/`union_combine` doing one add. That is the point — it makes
30//! the harness measure binding overhead rather than the user's own work.
31
32use apache_datasketches::tuple::generic::{TupleSketch, TupleSketchBuilder, TupleSummary};
33use std::time::{Duration, Instant};
34
35const LG_K: u8 = 12;
36const HOT_KEY_SPACE: u64 = 1 << 10;
37
38#[derive(Clone)]
39struct Count(u64);
40
41impl TupleSummary for Count {
42    type Update = ();
43    fn create(_: &()) -> Self {
44        Count(1)
45    }
46    fn union_combine(&mut self, other: &Self) {
47        self.0 += other.0;
48    }
49    fn intersection_combine(&mut self, other: &Self) {
50        self.0 += other.0;
51    }
52}
53
54fn report(label: &str, items: u64, elapsed: Duration, estimate: f64) {
55    let per_op = elapsed.as_secs_f64() * 1e9 / items as f64;
56    let rate = items as f64 / elapsed.as_secs_f64() / 1e6;
57    println!(
58        "{label:9}  {items:>12} items  {:>8.3} s  {per_op:>7.2} ns/op  {rate:>8.1} M/s  \
59         (estimate {estimate:.0})",
60        elapsed.as_secs_f64()
61    );
62}
63
64fn build() -> TupleSketch<Count> {
65    TupleSketchBuilder::new()
66        .lg_k(LG_K)
67        .build()
68        .expect("builder rejected fixed valid parameters")
69}
70
71fn bench_distinct(items: u64) {
72    let mut sketch = build();
73    let start = Instant::now();
74    for key in 0..items {
75        sketch.update_u64(key, &());
76    }
77    let elapsed = start.elapsed();
78    // Reading the estimate keeps the loop above from being optimised out.
79    report("distinct", items, elapsed, sketch.get_estimate());
80}
81
82fn bench_hot(items: u64) {
83    let mut sketch = build();
84    let start = Instant::now();
85    for i in 0..items {
86        sketch.update_u64(i % HOT_KEY_SPACE, &());
87    }
88    let elapsed = start.elapsed();
89    report("hot", items, elapsed, sketch.get_estimate());
90}
91
92fn main() {
93    let items: u64 = std::env::args()
94        .nth(1)
95        .map(|arg| arg.parse().expect("item count must be a positive integer"))
96        .unwrap_or(10_000_000);
97
98    println!("lg_k={LG_K} items={items}");
99    bench_distinct(items);
100    bench_hot(items);
101}