Skip to content

Commit 02454bf

Browse files
committed
perf: add fast-path bitmap_has_all that avoids full deserialization
For large hybrid bitmaps, bitmap_has_all now checks superset relationships directly on serialized buffers using container-level comparison instead of deserializing both bitmaps. This reduces bitmap_has_all_large_large from ~39µs to ~11µs (3.5x), and disjoint cases from ~20µs to ~932ns (22x).
1 parent 0731bab commit 02454bf

4 files changed

Lines changed: 475 additions & 12 deletions

File tree

src/common/io/src/bitmap.rs

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,81 @@ pub fn bitmap_stats(buf: &[u8]) -> Result<BitmapStats> {
798798
}
799799
}
800800

801+
pub fn bitmap_has_all(lhs: &[u8], rhs: &[u8]) -> Result<bool> {
802+
if rhs.is_empty() {
803+
return Ok(true);
804+
}
805+
if lhs.is_empty() {
806+
return Ok(false);
807+
}
808+
809+
// Both HybridLarge: use stats for early rejection, then check directly on serialized buffers.
810+
if is_hybrid_large(lhs) && is_hybrid_large(rhs) {
811+
let lhs_stats = reader::bitmap_stats(&lhs[HYBRID_HEADER_LEN..])?;
812+
let rhs_stats = reader::bitmap_stats(&rhs[HYBRID_HEADER_LEN..])?;
813+
if let (Some(lhs_min), Some(rhs_min)) = (lhs_stats.min, rhs_stats.min)
814+
&& lhs_min > rhs_min
815+
{
816+
return Ok(false);
817+
}
818+
if let (Some(rhs_max), Some(lhs_max)) = (rhs_stats.max, lhs_stats.max)
819+
&& rhs_max > lhs_max
820+
{
821+
return Ok(false);
822+
}
823+
if lhs_stats.len < rhs_stats.len {
824+
return Ok(false);
825+
}
826+
Ok(reader::bitmap_has_all(
827+
&lhs[HYBRID_HEADER_LEN..],
828+
&rhs[HYBRID_HEADER_LEN..],
829+
)?)
830+
} else if is_hybrid_large(lhs) || is_hybrid_large(rhs) {
831+
// Mixed path: one side is HybridLarge, the other is HybridSmall or Legacy.
832+
// Use stats for early rejection; if rhs is HybridSmall, iterate and probe lhs;
833+
// otherwise fall back to full deserialization.
834+
let lhs_stats = bitmap_stats(lhs)?;
835+
let rhs_stats = bitmap_stats(rhs)?;
836+
if let (Some(lhs_min), Some(rhs_min)) = (lhs_stats.min, rhs_stats.min)
837+
&& lhs_min > rhs_min
838+
{
839+
return Ok(false);
840+
}
841+
if let (Some(rhs_max), Some(lhs_max)) = (rhs_stats.max, lhs_stats.max)
842+
&& rhs_max > lhs_max
843+
{
844+
return Ok(false);
845+
}
846+
// lhs.len < rhs.len -> lhs cannot contain all of rhs
847+
if lhs_stats.len < rhs_stats.len {
848+
return Ok(false);
849+
}
850+
// rhs is HybridSmall: iterate its (≤31) values and probe lhs via bitmap_contains.
851+
// Each bitmap_contains call scans prefix buckets from the start, causing redundant
852+
// work across iterations. This could be improved with a hint/cursor mechanism that
853+
// tracks the last scanned position, but since HybridSmall holds at most 31 values
854+
// the overhead is bounded (~μs-level). Revisit if this path becomes a bottleneck.
855+
if is_hybrid_large(lhs) && is_hybrid(rhs) && !is_hybrid_large(rhs) {
856+
let rhs_bm = deserialize_bitmap(rhs)?;
857+
for value in rhs_bm.iter() {
858+
if !bitmap_contains(lhs, value)? {
859+
return Ok(false);
860+
}
861+
}
862+
return Ok(true);
863+
}
864+
// Fallback to deserialization and is_superset
865+
let lhs_bm = deserialize_bitmap(lhs)?;
866+
let rhs_bm = deserialize_bitmap(rhs)?;
867+
Ok(lhs_bm.is_superset(&rhs_bm))
868+
} else {
869+
// Both else: deserialize both
870+
let lhs_bm = deserialize_bitmap(lhs)?;
871+
let rhs_bm = deserialize_bitmap(rhs)?;
872+
Ok(lhs_bm.is_superset(&rhs_bm))
873+
}
874+
}
875+
801876
pub fn bitmap_has_any(lhs: &[u8], rhs: &[u8]) -> Result<bool> {
802877
if lhs.is_empty() || rhs.is_empty() {
803878
return Ok(false);
@@ -1893,4 +1968,204 @@ mod tests {
18931968
assert_eq!(stats.min, Some(42));
18941969
assert_eq!(stats.max, Some(100));
18951970
}
1971+
1972+
fn ground_truth_has_all(lhs: &[u8], rhs: &[u8]) -> bool {
1973+
let lhs_bm = deserialize_bitmap(lhs).unwrap();
1974+
let rhs_bm = deserialize_bitmap(rhs).unwrap();
1975+
lhs_bm.is_superset(&rhs_bm)
1976+
}
1977+
1978+
#[test]
1979+
fn test_bitmap_has_all() {
1980+
// Both HybridLarge, rhs is subset: 0..50000 contains 0..10000
1981+
let lhs = HybridBitmap::from_iter(0u64..50000);
1982+
let rhs = HybridBitmap::from_iter(0u64..10000);
1983+
let mut lhs_buf = Vec::new();
1984+
let mut rhs_buf = Vec::new();
1985+
lhs.serialize_into(&mut lhs_buf).unwrap();
1986+
rhs.serialize_into(&mut rhs_buf).unwrap();
1987+
assert_eq!(
1988+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
1989+
ground_truth_has_all(&lhs_buf, &rhs_buf)
1990+
);
1991+
assert!(bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
1992+
1993+
// Both HybridLarge, rhs is not subset: 0..50000 vs 40000..90000
1994+
// rhs has values 50000-89999 that are not in lhs
1995+
let lhs = HybridBitmap::from_iter(0u64..50000);
1996+
let rhs = HybridBitmap::from_iter(40000u64..90000);
1997+
let mut lhs_buf = Vec::new();
1998+
let mut rhs_buf = Vec::new();
1999+
lhs.serialize_into(&mut lhs_buf).unwrap();
2000+
rhs.serialize_into(&mut rhs_buf).unwrap();
2001+
assert_eq!(
2002+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2003+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2004+
);
2005+
assert!(!bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2006+
2007+
// Both HybridLarge, disjoint: 0..50000 vs 100000..150000
2008+
let lhs = HybridBitmap::from_iter(0u64..50000);
2009+
let rhs = HybridBitmap::from_iter(100000u64..150000);
2010+
let mut lhs_buf = Vec::new();
2011+
let mut rhs_buf = Vec::new();
2012+
lhs.serialize_into(&mut lhs_buf).unwrap();
2013+
rhs.serialize_into(&mut rhs_buf).unwrap();
2014+
assert_eq!(
2015+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2016+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2017+
);
2018+
assert!(!bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2019+
2020+
// Both HybridSmall, rhs is subset: 0..31 contains 5..15
2021+
let lhs = HybridBitmap::from_iter(0u64..31);
2022+
let rhs = HybridBitmap::from_iter(5u64..15);
2023+
let mut lhs_buf = Vec::new();
2024+
let mut rhs_buf = Vec::new();
2025+
lhs.serialize_into(&mut lhs_buf).unwrap();
2026+
rhs.serialize_into(&mut rhs_buf).unwrap();
2027+
assert_eq!(
2028+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2029+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2030+
);
2031+
assert!(bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2032+
2033+
// Both HybridSmall, rhs is not subset: 0..15 vs 10..25
2034+
// rhs has values 15-24 that are not in lhs
2035+
let lhs = HybridBitmap::from_iter(0u64..15);
2036+
let rhs = HybridBitmap::from_iter(10u64..25);
2037+
let mut lhs_buf = Vec::new();
2038+
let mut rhs_buf = Vec::new();
2039+
lhs.serialize_into(&mut lhs_buf).unwrap();
2040+
rhs.serialize_into(&mut rhs_buf).unwrap();
2041+
assert_eq!(
2042+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2043+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2044+
);
2045+
assert!(!bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2046+
2047+
// Mixed (HybridLarge lhs, HybridSmall rhs) with superset: 0..50000 contains 5..15
2048+
let lhs = HybridBitmap::from_iter(0u64..50000);
2049+
let rhs = HybridBitmap::from_iter(5u64..15);
2050+
let mut lhs_buf = Vec::new();
2051+
let mut rhs_buf = Vec::new();
2052+
lhs.serialize_into(&mut lhs_buf).unwrap();
2053+
rhs.serialize_into(&mut rhs_buf).unwrap();
2054+
assert_eq!(
2055+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2056+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2057+
);
2058+
assert!(bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2059+
2060+
// Mixed (HybridLarge lhs, HybridSmall rhs not subset): 0..50000 vs 49990..50010
2061+
// rhs has values 50000-50009 that are not in lhs
2062+
let lhs = HybridBitmap::from_iter(0u64..50000);
2063+
let rhs = HybridBitmap::from_iter(49990u64..50010);
2064+
let mut lhs_buf = Vec::new();
2065+
let mut rhs_buf = Vec::new();
2066+
lhs.serialize_into(&mut lhs_buf).unwrap();
2067+
rhs.serialize_into(&mut rhs_buf).unwrap();
2068+
assert_eq!(
2069+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2070+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2071+
);
2072+
assert!(!bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2073+
2074+
// Mixed (HybridSmall lhs, HybridLarge rhs): already rejected by len fast path
2075+
let lhs = HybridBitmap::from_iter(0u64..31);
2076+
let rhs = HybridBitmap::from_iter(0u64..50000);
2077+
let mut lhs_buf = Vec::new();
2078+
let mut rhs_buf = Vec::new();
2079+
lhs.serialize_into(&mut lhs_buf).unwrap();
2080+
rhs.serialize_into(&mut rhs_buf).unwrap();
2081+
assert_eq!(
2082+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2083+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2084+
);
2085+
assert!(!bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2086+
2087+
// Empty rhs: always true
2088+
let lhs = HybridBitmap::from_iter(0u64..50000);
2089+
let mut lhs_buf = Vec::new();
2090+
lhs.serialize_into(&mut lhs_buf).unwrap();
2091+
assert!(bitmap_has_all(&lhs_buf, &[]).unwrap());
2092+
2093+
// Empty lhs with non-empty rhs: always false
2094+
let rhs = HybridBitmap::from_iter(0u64..50000);
2095+
let mut rhs_buf = Vec::new();
2096+
rhs.serialize_into(&mut rhs_buf).unwrap();
2097+
assert!(!bitmap_has_all(&[], &rhs_buf).unwrap());
2098+
2099+
// Both empty: true
2100+
assert!(bitmap_has_all(&[], &[]).unwrap());
2101+
2102+
// Mixed (Legacy lhs, HybridLarge rhs)
2103+
let mut lhs_tree = RoaringTreemap::new();
2104+
for v in 0u64..50000 {
2105+
lhs_tree.insert(v);
2106+
}
2107+
let rhs = HybridBitmap::from_iter(0u64..10000);
2108+
let mut lhs_buf = Vec::new();
2109+
let mut rhs_buf = Vec::new();
2110+
lhs_tree.serialize_into(&mut lhs_buf).unwrap();
2111+
rhs.serialize_into(&mut rhs_buf).unwrap();
2112+
assert_eq!(
2113+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2114+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2115+
);
2116+
2117+
// Mixed (HybridLarge lhs, Legacy rhs)
2118+
let lhs = HybridBitmap::from_iter(0u64..10000);
2119+
let mut rhs_tree = RoaringTreemap::new();
2120+
for v in 0u64..50000 {
2121+
rhs_tree.insert(v);
2122+
}
2123+
2124+
let mut lhs_buf = Vec::new();
2125+
let mut rhs_buf = Vec::new();
2126+
lhs.serialize_into(&mut lhs_buf).unwrap();
2127+
rhs_tree.serialize_into(&mut rhs_buf).unwrap();
2128+
assert_eq!(
2129+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2130+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2131+
);
2132+
2133+
// Both Legacy
2134+
let mut lhs_tree = RoaringTreemap::new();
2135+
for v in 0u64..100 {
2136+
lhs_tree.insert(v);
2137+
}
2138+
let mut rhs_tree = RoaringTreemap::new();
2139+
for v in 50u64..80 {
2140+
rhs_tree.insert(v);
2141+
}
2142+
let mut lhs_buf = Vec::new();
2143+
let mut rhs_buf = Vec::new();
2144+
lhs_tree.serialize_into(&mut lhs_buf).unwrap();
2145+
rhs_tree.serialize_into(&mut rhs_buf).unwrap();
2146+
assert_eq!(
2147+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2148+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2149+
);
2150+
assert!(bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2151+
2152+
// Both Legacy, rhs not subset
2153+
let mut lhs_tree = RoaringTreemap::new();
2154+
for v in 0u64..100 {
2155+
lhs_tree.insert(v);
2156+
}
2157+
let mut rhs_tree = RoaringTreemap::new();
2158+
for v in 50u64..150 {
2159+
rhs_tree.insert(v);
2160+
}
2161+
let mut lhs_buf = Vec::new();
2162+
let mut rhs_buf = Vec::new();
2163+
lhs_tree.serialize_into(&mut lhs_buf).unwrap();
2164+
rhs_tree.serialize_into(&mut rhs_buf).unwrap();
2165+
assert_eq!(
2166+
bitmap_has_all(&lhs_buf, &rhs_buf).unwrap(),
2167+
ground_truth_has_all(&lhs_buf, &rhs_buf)
2168+
);
2169+
assert!(!bitmap_has_all(&lhs_buf, &rhs_buf).unwrap());
2170+
}
18962171
}

0 commit comments

Comments
 (0)