Skip to content

Commit 4bfba50

Browse files
committed
perf: add fast-path bitmap_has_any that avoids full deserialization
For large hybrid bitmaps, bitmap_has_any now checks intersection directly on the serialized buffers using container-level binary search instead of deserializing both bitmaps and computing full intersection. This reduces bitmap_has_any_large_large from ~26µs to ~2.5µs (10x), and disjoint cases from ~20µs to ~963ns (21x).
1 parent 38510eb commit 4bfba50

4 files changed

Lines changed: 528 additions & 12 deletions

File tree

src/common/io/src/bitmap.rs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ use roaring::RoaringTreemap;
2828
use roaring::treemap::Iter;
2929
use smallvec::SmallVec;
3030

31+
pub use crate::bitmap::reader::BitmapStats;
32+
3133
mod reader;
3234

3335
// https://github.com/ClickHouse/ClickHouse/blob/516a6ed6f8bd8c5f6eed3a10e9037580b2fb6152/src/AggregateFunctions/AggregateFunctionGroupBitmapData.h#L914
@@ -816,6 +818,102 @@ pub fn bitmap_max(buf: &[u8]) -> Result<Option<u64>> {
816818
}
817819
}
818820

821+
pub fn bitmap_stats(buf: &[u8]) -> Result<BitmapStats> {
822+
if buf.is_empty() {
823+
return Ok(BitmapStats {
824+
len: 0,
825+
min: None,
826+
max: None,
827+
});
828+
}
829+
if is_hybrid_large(buf) {
830+
Ok(reader::bitmap_stats(&buf[HYBRID_HEADER_LEN..])?)
831+
} else {
832+
let bm = deserialize_bitmap(buf)?;
833+
Ok(BitmapStats {
834+
len: bm.len() as u64,
835+
min: bm.min(),
836+
max: bm.max(),
837+
})
838+
}
839+
}
840+
841+
pub fn bitmap_has_any(lhs: &[u8], rhs: &[u8]) -> Result<bool> {
842+
if lhs.is_empty() || rhs.is_empty() {
843+
return Ok(false);
844+
}
845+
846+
validate_serialized_bitmap_header_and_length(lhs)?;
847+
validate_serialized_bitmap_header_and_length(rhs)?;
848+
849+
// Stats fast path: range rejection
850+
// If lhs.max < rhs.min OR rhs.max < lhs.min, no overlap possible
851+
if is_hybrid_large(lhs) && is_hybrid_large(rhs) {
852+
let lhs_stats = reader::bitmap_stats(&lhs[HYBRID_HEADER_LEN..])?;
853+
let rhs_stats = reader::bitmap_stats(&rhs[HYBRID_HEADER_LEN..])?;
854+
if let (Some(lhs_min), Some(rhs_max)) = (lhs_stats.min, rhs_stats.max)
855+
&& lhs_min > rhs_max
856+
{
857+
return Ok(false);
858+
}
859+
if let (Some(rhs_min), Some(lhs_max)) = (rhs_stats.min, lhs_stats.max)
860+
&& rhs_min > lhs_max
861+
{
862+
return Ok(false);
863+
}
864+
Ok(reader::bitmap_has_any(
865+
&lhs[HYBRID_HEADER_LEN..],
866+
&rhs[HYBRID_HEADER_LEN..],
867+
)?)
868+
} else if is_hybrid_large(lhs) || is_hybrid_large(rhs) {
869+
// Normalize: large is the HybridLarge side, other is the non-HybridLarge side
870+
let (large, other) = if is_hybrid_large(lhs) {
871+
(lhs, rhs)
872+
} else {
873+
(rhs, lhs)
874+
};
875+
876+
// Stats fast path: range rejection
877+
let large_stats = reader::bitmap_stats(&large[HYBRID_HEADER_LEN..])?;
878+
let other_stats = bitmap_stats(other)?;
879+
if let (Some(large_min), Some(other_max)) = (large_stats.min, other_stats.max)
880+
&& large_min > other_max
881+
{
882+
return Ok(false);
883+
}
884+
if let (Some(other_min), Some(large_max)) = (other_stats.min, large_stats.max)
885+
&& other_min > large_max
886+
{
887+
return Ok(false);
888+
}
889+
890+
// other is HybridSmall: iterate its (≤31) values through bitmap_contains on large.
891+
// Each bitmap_contains call scans prefix buckets from the start, causing redundant
892+
// work across iterations. This could be improved with a hint/cursor mechanism that
893+
// tracks the last scanned position, but since HybridSmall holds at most 31 values
894+
// the overhead is bounded (~μs-level). Revisit if this path becomes a bottleneck.
895+
if is_hybrid(other) && !is_hybrid_large(other) {
896+
let other_bm = deserialize_bitmap(other)?;
897+
for value in other_bm.iter() {
898+
if bitmap_contains(large, value)? {
899+
return Ok(true);
900+
}
901+
}
902+
return Ok(false);
903+
}
904+
905+
// other is Legacy (potentially large): fallback to full deserialization
906+
let lhs_bm = deserialize_bitmap(lhs)?;
907+
let rhs_bm = deserialize_bitmap(rhs)?;
908+
Ok(lhs_bm.intersection_len(&rhs_bm) != 0)
909+
} else {
910+
// Both else: deserialize both
911+
let lhs_bm = deserialize_bitmap(lhs)?;
912+
let rhs_bm = deserialize_bitmap(rhs)?;
913+
Ok(lhs_bm.intersection_len(&rhs_bm) != 0)
914+
}
915+
}
916+
819917
fn parse_bitmap_rhs(buf: &[u8]) -> Result<BitmapRhsView<'_>> {
820918
if buf.is_empty() {
821919
return Ok(BitmapRhsView::Empty);
@@ -1647,4 +1745,195 @@ mod tests {
16471745
// Empty buffer
16481746
assert_eq!(bitmap_max(&[]).unwrap(), None);
16491747
}
1748+
1749+
fn ground_truth_has_any(lhs: &[u8], rhs: &[u8]) -> bool {
1750+
let lhs_bm = deserialize_bitmap(lhs).unwrap();
1751+
let rhs_bm = deserialize_bitmap(rhs).unwrap();
1752+
lhs_bm.intersection_len(&rhs_bm) != 0
1753+
}
1754+
1755+
#[test]
1756+
fn test_bitmap_has_any() {
1757+
// Both HybridLarge with overlap: 0..50000 and 40000..90000 -> overlap at 40000-49999
1758+
let lhs = HybridBitmap::from_iter(0u64..50000);
1759+
let rhs = HybridBitmap::from_iter(40000u64..90000);
1760+
let mut lhs_buf = Vec::new();
1761+
let mut rhs_buf = Vec::new();
1762+
lhs.serialize_into(&mut lhs_buf).unwrap();
1763+
rhs.serialize_into(&mut rhs_buf).unwrap();
1764+
assert_eq!(
1765+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1766+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1767+
);
1768+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1769+
1770+
// Both HybridLarge disjoint: 0..50000 and 100000..150000
1771+
let lhs = HybridBitmap::from_iter(0u64..50000);
1772+
let rhs = HybridBitmap::from_iter(100000u64..150000);
1773+
let mut lhs_buf = Vec::new();
1774+
let mut rhs_buf = Vec::new();
1775+
lhs.serialize_into(&mut lhs_buf).unwrap();
1776+
rhs.serialize_into(&mut rhs_buf).unwrap();
1777+
assert_eq!(
1778+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1779+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1780+
);
1781+
assert!(!bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1782+
1783+
// Both HybridSmall with overlap: 0..31 and 20..51 -> overlap at 20-30
1784+
let lhs = HybridBitmap::from_iter(0u64..31);
1785+
let rhs = HybridBitmap::from_iter(20u64..51);
1786+
let mut lhs_buf = Vec::new();
1787+
let mut rhs_buf = Vec::new();
1788+
lhs.serialize_into(&mut lhs_buf).unwrap();
1789+
rhs.serialize_into(&mut rhs_buf).unwrap();
1790+
assert_eq!(
1791+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1792+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1793+
);
1794+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1795+
1796+
// Both HybridSmall disjoint: 0..15 and 20..35
1797+
let lhs = HybridBitmap::from_iter(0u64..15);
1798+
let rhs = HybridBitmap::from_iter(20u64..35);
1799+
let mut lhs_buf = Vec::new();
1800+
let mut rhs_buf = Vec::new();
1801+
lhs.serialize_into(&mut lhs_buf).unwrap();
1802+
rhs.serialize_into(&mut rhs_buf).unwrap();
1803+
assert_eq!(
1804+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1805+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1806+
);
1807+
assert!(!bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1808+
1809+
// Mixed (HybridLarge lhs, HybridSmall rhs) with overlap: 0..50000 and 10..41
1810+
let lhs = HybridBitmap::from_iter(0u64..50000);
1811+
let rhs = HybridBitmap::from_iter(10u64..41);
1812+
let mut lhs_buf = Vec::new();
1813+
let mut rhs_buf = Vec::new();
1814+
lhs.serialize_into(&mut lhs_buf).unwrap();
1815+
rhs.serialize_into(&mut rhs_buf).unwrap();
1816+
assert_eq!(
1817+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1818+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1819+
);
1820+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1821+
1822+
// Mixed (HybridSmall lhs, HybridLarge rhs) with overlap: 10..41 and 0..50000
1823+
let lhs = HybridBitmap::from_iter(10u64..41);
1824+
let rhs = HybridBitmap::from_iter(0u64..50000);
1825+
let mut lhs_buf = Vec::new();
1826+
let mut rhs_buf = Vec::new();
1827+
lhs.serialize_into(&mut lhs_buf).unwrap();
1828+
rhs.serialize_into(&mut rhs_buf).unwrap();
1829+
assert_eq!(
1830+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1831+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1832+
);
1833+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1834+
1835+
// Empty lhs
1836+
let rhs = HybridBitmap::from_iter(0u64..50000);
1837+
let mut rhs_buf = Vec::new();
1838+
rhs.serialize_into(&mut rhs_buf).unwrap();
1839+
assert!(!bitmap_has_any(&[], &rhs_buf).unwrap());
1840+
1841+
// Empty rhs
1842+
let lhs = HybridBitmap::from_iter(0u64..50000);
1843+
let mut lhs_buf = Vec::new();
1844+
lhs.serialize_into(&mut lhs_buf).unwrap();
1845+
assert!(!bitmap_has_any(&lhs_buf, &[]).unwrap());
1846+
1847+
// Both empty
1848+
assert!(!bitmap_has_any(&[], &[]).unwrap());
1849+
1850+
// Mixed (Legacy lhs, HybridLarge rhs) with overlap
1851+
let mut lhs_tree = RoaringTreemap::new();
1852+
for v in [0u64, 2500, 40000] {
1853+
lhs_tree.insert(v);
1854+
}
1855+
let rhs = HybridBitmap::from_iter(0u64..50000);
1856+
let mut lhs_buf = Vec::new();
1857+
let mut rhs_buf = Vec::new();
1858+
lhs_tree.serialize_into(&mut lhs_buf).unwrap();
1859+
rhs.serialize_into(&mut rhs_buf).unwrap();
1860+
assert_eq!(
1861+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1862+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1863+
);
1864+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1865+
1866+
// Mixed (HybridLarge lhs, Legacy rhs) with overlap
1867+
let lhs = HybridBitmap::from_iter(0u64..50000);
1868+
let mut rhs_tree = RoaringTreemap::new();
1869+
for v in [0u64, 2500, 40000] {
1870+
rhs_tree.insert(v);
1871+
}
1872+
let mut lhs_buf = Vec::new();
1873+
let mut rhs_buf = Vec::new();
1874+
lhs.serialize_into(&mut lhs_buf).unwrap();
1875+
rhs_tree.serialize_into(&mut rhs_buf).unwrap();
1876+
assert_eq!(
1877+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1878+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1879+
);
1880+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1881+
1882+
// Both Legacy with overlap
1883+
let mut lhs_tree = RoaringTreemap::new();
1884+
for v in 0u64..100 {
1885+
lhs_tree.insert(v);
1886+
}
1887+
let mut rhs_tree = RoaringTreemap::new();
1888+
for v in 50u64..150 {
1889+
rhs_tree.insert(v);
1890+
}
1891+
let mut lhs_buf = Vec::new();
1892+
let mut rhs_buf = Vec::new();
1893+
lhs_tree.serialize_into(&mut lhs_buf).unwrap();
1894+
rhs_tree.serialize_into(&mut rhs_buf).unwrap();
1895+
assert_eq!(
1896+
bitmap_has_any(&lhs_buf, &rhs_buf).unwrap(),
1897+
ground_truth_has_any(&lhs_buf, &rhs_buf)
1898+
);
1899+
assert!(bitmap_has_any(&lhs_buf, &rhs_buf).unwrap());
1900+
}
1901+
1902+
#[test]
1903+
fn test_bitmap_stats() {
1904+
// HybridLarge stats
1905+
let large = HybridBitmap::from_iter(0u64..50000);
1906+
let mut buf = Vec::new();
1907+
large.serialize_into(&mut buf).unwrap();
1908+
let stats = bitmap_stats(&buf).unwrap();
1909+
assert_eq!(stats.len, 50000);
1910+
assert_eq!(stats.min, Some(0));
1911+
assert_eq!(stats.max, Some(49999));
1912+
1913+
// HybridSmall stats
1914+
let small = HybridBitmap::from_iter(100u64..131);
1915+
let mut buf = Vec::new();
1916+
small.serialize_into(&mut buf).unwrap();
1917+
let stats = bitmap_stats(&buf).unwrap();
1918+
assert_eq!(stats.len, 31);
1919+
assert_eq!(stats.min, Some(100));
1920+
assert_eq!(stats.max, Some(130));
1921+
1922+
// Empty stats
1923+
let stats = bitmap_stats(&[]).unwrap();
1924+
assert_eq!(stats.len, 0);
1925+
assert_eq!(stats.min, None);
1926+
assert_eq!(stats.max, None);
1927+
1928+
// Legacy stats
1929+
let mut tree = RoaringTreemap::new();
1930+
tree.insert(42);
1931+
tree.insert(100);
1932+
let mut buf = Vec::new();
1933+
tree.serialize_into(&mut buf).unwrap();
1934+
let stats = bitmap_stats(&buf).unwrap();
1935+
assert_eq!(stats.len, 2);
1936+
assert_eq!(stats.min, Some(42));
1937+
assert_eq!(stats.max, Some(100));
1938+
}
16501939
}

0 commit comments

Comments
 (0)