Skip to main content

estimate_intersect

Function estimate_intersect 

Source
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
Hide additional 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}