bench_tuple_update/bench_tuple_update.rs
1//! Throughput harness for `ArrayOfDoublesSketch::update_*`.
2//!
3//! This exists because the ArrayOfDoubles update path crosses the FFI
4//! boundary once per item and is the only hot loop in the crate where
5//! per-call overhead in the shim is measurable against upstream C++. Run it
6//! before and after any change to that path.
7//!
8//! Run with (release matters — a debug build measures nothing useful):
9//! cargo run --release --example bench_tuple_update --features tuple
10//! cargo run --release --example bench_tuple_update --features tuple -- 100000000
11//!
12//! Fixed parameters, so numbers are comparable across runs: `lg_k = 12`,
13//! `num_values = 3`, `resize_factor` and `p` at their defaults. The item
14//! count defaults to 10M and can be overridden as the first argument.
15//!
16//! This measures absolute throughput. To get the number that actually matters
17//! — how much of the cost is *this binding* rather than the algorithm — run
18//! the native C++ counterpart on the same item count and divide:
19//!
20//! ./benches/cpp_reference/run.sh 10000000
21//!
22//! That program mirrors this one's parameters and scenarios exactly. Keep the
23//! two in sync: if you change `LG_K`, `NUM_VALUES` or `HOT_KEY_SPACE` here,
24//! change them there too. Both print the sketch estimate, and the estimates
25//! must match — that is the cheap check that they are doing the same work.
26//!
27//! Two scenarios, because they exercise different halves of upstream's
28//! `update_tuple_sketch::update`:
29//!
30//! - `distinct` — every key is new. Once theta drops below 1.0 most keys are
31//! rejected by `hash_and_screen`, which returns *before* upstream ever
32//! reads the values. Per-call work in the shim that happens ahead of that
33//! screen is pure waste here.
34//! - `hot` — keys drawn from a space small enough to stay fully retained, so
35//! every call reaches the summary-combine path.
36
37use apache_datasketches::tuple::ArrayOfDoublesSketchBuilder;
38use std::time::{Duration, Instant};
39
40const LG_K: u8 = 12;
41const NUM_VALUES: u8 = 3;
42const HOT_KEY_SPACE: u64 = 1 << 10;
43
44/// Values passed on every update. Length must equal `NUM_VALUES`.
45const VALUES: [f64; 3] = [1.0, 2.0, 3.0];
46
47fn report(label: &str, items: u64, elapsed: Duration, estimate: f64) {
48 let nanos = elapsed.as_secs_f64() * 1e9;
49 let per_op = nanos / items as f64;
50 let rate = items as f64 / elapsed.as_secs_f64() / 1e6;
51 println!(
52 "{label:9} {items:>12} items {:>8.3} s {per_op:>7.2} ns/op {rate:>8.1} M/s \
53 (estimate {estimate:.0})",
54 elapsed.as_secs_f64()
55 );
56}
57
58fn bench_distinct(items: u64) {
59 let mut sketch = ArrayOfDoublesSketchBuilder::new()
60 .lg_k(LG_K)
61 .num_values(NUM_VALUES)
62 .build()
63 .expect("builder rejected fixed valid parameters");
64
65 let start = Instant::now();
66 for key in 0..items {
67 sketch
68 .update_u64(key, &VALUES)
69 .expect("update rejected a correctly-sized value slice");
70 }
71 let elapsed = start.elapsed();
72
73 // Reading the estimate keeps the loop above from being optimised out.
74 report("distinct", items, elapsed, sketch.get_estimate());
75}
76
77fn bench_hot(items: u64) {
78 let mut sketch = ArrayOfDoublesSketchBuilder::new()
79 .lg_k(LG_K)
80 .num_values(NUM_VALUES)
81 .build()
82 .expect("builder rejected fixed valid parameters");
83
84 let start = Instant::now();
85 for i in 0..items {
86 sketch
87 .update_u64(i % HOT_KEY_SPACE, &VALUES)
88 .expect("update rejected a correctly-sized value slice");
89 }
90 let elapsed = start.elapsed();
91
92 report("hot", items, elapsed, sketch.get_estimate());
93}
94
95fn main() {
96 let items: u64 = std::env::args()
97 .nth(1)
98 .map(|arg| arg.parse().expect("item count must be a positive integer"))
99 .unwrap_or(10_000_000);
100
101 println!("lg_k={LG_K} num_values={NUM_VALUES} items={items}");
102 bench_distinct(items);
103 bench_hot(items);
104}