pub fn estimate_intersect(
a: &HyperLogLog,
b: &HyperLogLog,
) -> Result<f64, HllError>Expand description
Distinct count of the intersection via inclusion-exclusion. Clamps to
>= 0 since a negative estimate is a hard signal of large relative error.
Examples found in repository?
examples/sample_app.rs (line 189)
175fn cross_venue_overlap(tape: &[Event]) {
176 use subms_hyperloglog::{estimate_intersect, estimate_union, intersect_error_bound};
177 println!("\n== venues: account reach and overlap ==");
178
179 let mut a = HyperLogLog::new(14);
180 let mut b = HyperLogLog::new(14);
181 for e in tape {
182 if e.venue == 0 {
183 a.add_u64(e.account);
184 } else {
185 b.add_u64(e.account);
186 }
187 }
188 let union = estimate_union(&a, &b).expect("same precision");
189 let inter = estimate_intersect(&a, &b).expect("same precision");
190 let bound = intersect_error_bound(&a, &b).expect("same precision");
191 println!(" venue 0: {:>7.0} accounts", a.estimate());
192 println!(" venue 1: {:>7.0} accounts", b.estimate());
193 println!(" reach: {union:>7.0} (true 50000)");
194 println!(" both: {inter:>7.0} (true 10000) +/- {bound:.0}");
195 assert!(
196 (union - 50_000.0).abs() / 50_000.0 < 0.05,
197 "reach within 5%, got {union}"
198 );
199 assert!(inter > 0.0, "a 10k overlap must survive the subtraction");
200}More examples
examples/perf_features.rs (line 261)
116fn main() -> io::Result<()> {
117 let ks = keys();
118
119 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
120 .join("..")
121 .join(".subms")
122 .join("features")
123 .join("rust.json");
124 let existing = std::fs::read_to_string(&path).unwrap_or_default();
125 let mut manifest = SubMsFeatureManifest::load_str("rust", &existing);
126 // Stamp the box these numbers came from. The bench runs wherever it is
127 // invoked, so an unstamped manifest is indistinguishable from a fleet
128 // capture; the renderer will not publish one it cannot attribute.
129 let (source, instance) = SubMsP99Source::from_env();
130 manifest.set_p99_source(source, instance.as_deref());
131
132 // The baseline is base `add`, the per-op path. NOT base `estimate`: that
133 // folds all 2^p registers, so classifying a per-key feature against it would
134 // let anything look free.
135 let mut base = HyperLogLog::new(CANON_P);
136 let base_p50 = keyed_p50(&mut base, &ks[..OPS], |h, k| {
137 h.add(k);
138 });
139 eprintln!("base add p50: {base_p50}ns");
140
141 // ---------- sparse: a linear entry list until it earns the dense array ----------
142 #[cfg(feature = "sparse")]
143 {
144 use subms_hyperloglog::SparseHyperLogLog;
145 // Swept over SPARSE LIST LENGTH, not over precision. `add` linear-probes
146 // the list, so length is the cost driver; precision only sets it
147 // indirectly through the `m/4` promotion threshold, and swept that way
148 // the curve is a step rather than a slope. At p=12 and p=15 the
149 // structure promotes early, so BOTH low points measure the dense floor
150 // (100ns) rather than a small sparse probe, and at p=18 the list is
151 // capped by the key count instead of by the threshold. The resulting
152 // ratio landed either side of the classifier's guard - 40x in Rust,
153 // 23x in Java - which is a measurement artefact, not a real disagreement.
154 //
155 // `with_threshold` exists for exactly this: pin promotion out of reach
156 // and add n keys, and the swept axis IS the list length.
157 //
158 // The list is built to length n OUTSIDE the timed region, and the timed
159 // ops are re-adds of keys already in it - a fixed OPS of them at every
160 // size, so the op count is constant and the scan length is the only
161 // thing varying. Re-adding rather than adding fresh keys keeps the list
162 // from growing under measurement.
163 let sw = sweep_sizes("sparse/add(list-len)", &LIST_LENS, |n| {
164 let mut s = SparseHyperLogLog::with_threshold(CANON_P, n + 1);
165 for k in &ks[..n] {
166 s.add(k);
167 }
168 let mut h = SubMsPerfHarness::new("hll-feature", "rust");
169 let st = h.stage("op", OPS);
170 for i in 0..OPS {
171 let k = &ks[(i * 7919) % n];
172 st.time(|| s.add(k));
173 }
174 stat(&h, true)
175 });
176 // PINNED structural when the ratio test cannot carry it. `add`
177 // linear-probes the sparse list, so it is O(entries) from the source and
178 // the sweep above is monotonic and strongly rising. What it is not is
179 // 32x: a long scan runs ~0.34 ns/element against ~0.93 for a short one,
180 // so a true O(n) op measures ~23x over a 64x span and falls under the
181 // classifier's 0.5 guard. Publishing that as hot-path would tell a
182 // reader the probe is free at high precision. It is not, and the pin
183 // says a human decided rather than dressing the decision as measured.
184 let (cat, reason) = classify_feature(
185 &sw,
186 Some(base_p50),
187 Some(subms::SubMsFeatureCategory::Structural),
188 );
189
190 let mut s = SparseHyperLogLog::new(CANON_P);
191 let mut p99 = BTreeMap::new();
192 p99.insert(
193 "add".to_string(),
194 keyed_p99(&mut s, &ks[..OPS], |x, k| {
195 x.add(k);
196 }),
197 );
198 p99.insert(
199 "estimate".to_string(),
200 bulk(
201 || {
202 let mut x = SparseHyperLogLog::new(CANON_P);
203 for k in &ks[..OPS] {
204 x.add(k);
205 }
206 x
207 },
208 |x| _ = x.estimate(),
209 false,
210 ),
211 );
212 manifest.set_feature("sparse", cat, &p99, &reason);
213 }
214
215 // ---------- union-intersect: pairwise folds over both register arrays ----------
216 #[cfg(feature = "union-intersect")]
217 {
218 use subms_hyperloglog::{estimate_intersect, estimate_union};
219 // Both HLLs are built by `setup`, outside the timed region. A union is a
220 // pure read of two register arrays, so repeating it does identical work.
221 // Filled with `m` keys, not a fixed count. OCCUPANCY has to be held
222 // constant or it, not size, is what the sweep measures: `estimate` costs
223 // `2f64.powi(-r)` per register and `powi(0)` takes a fast path, so a
224 // fixed key set against a growing array leaves 92% of registers zero at
225 // p=18 against 0% at p=12. That reads as a per-register cost falling
226 // with size, and it compressed a triple-O(m) op to 26x over 64x.
227 let build = |p: u32| {
228 let n = regs(p);
229 let mut a = HyperLogLog::new(p);
230 let mut b = HyperLogLog::new(p);
231 for (i, k) in ks[..n].iter().enumerate() {
232 a.add(k);
233 if i % 2 == 0 {
234 b.add(k);
235 }
236 }
237 (a, b)
238 };
239 let sw = sweep("union-intersect/estimate_union", |p| {
240 bulk(
241 || build(p),
242 |(a, b)| _ = estimate_union(a, b).expect("same precision"),
243 true,
244 )
245 });
246 let (cat, reason) = classify_feature(&sw, Some(base_p50), None);
247
248 let mut p99 = BTreeMap::new();
249 p99.insert(
250 "union".to_string(),
251 bulk(
252 || build(CANON_P),
253 |(a, b)| _ = estimate_union(a, b).expect("same precision"),
254 false,
255 ),
256 );
257 p99.insert(
258 "intersect".to_string(),
259 bulk(
260 || build(CANON_P),
261 |(a, b)| _ = estimate_intersect(a, b).expect("same precision"),
262 false,
263 ),
264 );
265 manifest.set_feature("union-intersect", cat, &p99, &reason);
266 }
267
268 std::fs::create_dir_all(path.parent().unwrap())?;
269 std::fs::write(&path, manifest.to_json())?;
270 io::stdout().write_all(manifest.to_json().as_bytes())?;
271 Ok(())
272}