Skip to content

Commit 1402306

Browse files
committed
perf: add fast-path bitmap_min that avoids full deserialization
For large hybrid bitmaps, bitmap_min now reads the minimum value directly from the serialized buffer (first element of the first container) instead of deserializing the entire roaring treemap. This reduces bitmap_min_large from ~10.3µs to ~562ns (18x speedup).
1 parent 5c13203 commit 1402306

4 files changed

Lines changed: 112 additions & 8 deletions

File tree

src/common/io/src/bitmap.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,17 @@ pub fn bitmap_contains(buf: &[u8], value: u64) -> Result<bool> {
754754
}
755755
}
756756

757+
pub fn bitmap_min(buf: &[u8]) -> Result<Option<u64>> {
758+
if buf.is_empty() {
759+
return Ok(None);
760+
}
761+
if is_hybrid_large(buf) {
762+
Ok(reader::bitmap_min(&buf[HYBRID_HEADER_LEN..])?)
763+
} else {
764+
Ok(deserialize_bitmap(buf)?.min())
765+
}
766+
}
767+
757768
fn parse_bitmap_rhs(buf: &[u8]) -> Result<BitmapRhsView<'_>> {
758769
if buf.is_empty() {
759770
return Ok(BitmapRhsView::Empty);
@@ -1490,4 +1501,51 @@ mod tests {
14901501
// Empty buffer
14911502
assert!(!bitmap_contains(&[], 42).unwrap());
14921503
}
1504+
1505+
#[test]
1506+
fn test_bitmap_min() {
1507+
// HybridLarge: spanning multiple containers
1508+
let large = HybridBitmap::from_iter(
1509+
[0u64, 2500, 65535, 65536, 65536 + 2500, 131071]
1510+
.into_iter()
1511+
.chain((0..40000).map(|v| v + 200000)),
1512+
);
1513+
let mut buf = Vec::new();
1514+
large.serialize_into(&mut buf).unwrap();
1515+
assert_eq!(bitmap_min(&buf).unwrap(), Some(0));
1516+
1517+
// HybridLarge with array container at max capacity (cardinality = 4096)
1518+
let boundary = HybridBitmap::from_iter(0u64..4096);
1519+
let mut buf = Vec::new();
1520+
boundary.serialize_into(&mut buf).unwrap();
1521+
assert_eq!(bitmap_min(&buf).unwrap(), Some(0));
1522+
1523+
// HybridLarge with multiple containers: array (4096) + bitmap (>4096)
1524+
let mixed = HybridBitmap::from_iter((0u64..4096).chain(65536..106496));
1525+
let mut buf = Vec::new();
1526+
mixed.serialize_into(&mut buf).unwrap();
1527+
assert_eq!(bitmap_min(&buf).unwrap(), Some(0));
1528+
1529+
// HybridSmall
1530+
let small = HybridBitmap::from_iter(0u64..31);
1531+
let mut buf = Vec::new();
1532+
small.serialize_into(&mut buf).unwrap();
1533+
assert_eq!(bitmap_min(&buf).unwrap(), Some(0));
1534+
1535+
// HybridSmall with non-zero start
1536+
let small = HybridBitmap::from_iter(100u64..131);
1537+
let mut buf = Vec::new();
1538+
small.serialize_into(&mut buf).unwrap();
1539+
assert_eq!(bitmap_min(&buf).unwrap(), Some(100));
1540+
1541+
// Legacy
1542+
let mut tree = RoaringTreemap::new();
1543+
tree.insert(42);
1544+
let mut buf = Vec::new();
1545+
tree.serialize_into(&mut buf).unwrap();
1546+
assert_eq!(bitmap_min(&buf).unwrap(), Some(42));
1547+
1548+
// Empty buffer
1549+
assert_eq!(bitmap_min(&[]).unwrap(), None);
1550+
}
14931551
}

src/common/io/src/bitmap/reader.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,52 @@ fn bitmap_container_contains(data: &[u8], low16: u16) -> io::Result<bool> {
291291
Ok(word & (1 << bit_index) != 0)
292292
}
293293

294+
pub(crate) fn bitmap_min(buf: &[u8]) -> io::Result<Option<u64>> {
295+
let tree = TreemapReader::new(buf)?;
296+
let bitmap = match tree.iter().next() {
297+
None => return Ok(None),
298+
Some(b) => b?,
299+
};
300+
if bitmap.containers() == 0 {
301+
return Ok(None);
302+
}
303+
let desc = bitmap.description(0)?;
304+
let offset = bitmap.container_offset(0)?;
305+
let container_data = &bitmap.bitmap_buf()[offset..];
306+
let prefix = bitmap.prefix() as u64;
307+
let container_key = desc.prefix as u64;
308+
let cardinality = desc.cardinality();
309+
let low16 = if cardinality <= ARRAY_LIMIT {
310+
array_container_first(container_data, cardinality)?
311+
} else {
312+
bitmap_container_first(container_data)?
313+
};
314+
Ok(Some(prefix << 32 | container_key << 16 | low16 as u64))
315+
}
316+
317+
fn array_container_first(data: &[u8], cardinality: usize) -> io::Result<u16> {
318+
if data.len() < cardinality * 2 {
319+
return Err(Error::other("array container too short"));
320+
}
321+
Ok(u16::from_le_bytes(data[0..2].try_into().unwrap()))
322+
}
323+
324+
fn bitmap_container_first(data: &[u8]) -> io::Result<u16> {
325+
if data.len() < BITMAP_BYTES {
326+
return Err(Error::other("bitmap container too short"));
327+
}
328+
// Find the lowest set bit in the 1024-word bitmap
329+
for word_index in 0..BITMAP_WORDS {
330+
let start = word_index * WORD_BYTES;
331+
let word = u64::from_le_bytes(data[start..start + WORD_BYTES].try_into().unwrap());
332+
if word != 0 {
333+
return Ok((word_index * WORD_BITS + word.trailing_zeros() as usize) as u16);
334+
}
335+
}
336+
// All zeros — shouldn't happen for a valid bitmap container
337+
Err(Error::other("bitmap container has no set bits"))
338+
}
339+
294340
pub fn intersection_with_serialized(tree: &mut RoaringTreemap, buf: &[u8]) -> io::Result<()> {
295341
use std::cmp::Ordering::*;
296342
let rhs = TreemapReader::new(buf)?;

src/common/io/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub use bitmap::HYBRID_VERSION;
5454
pub use bitmap::HybridBitmap;
5555
pub use bitmap::LARGE_THRESHOLD;
5656
pub use bitmap::bitmap_contains;
57+
pub use bitmap::bitmap_min;
5758
pub use bitmap::deserialize_bitmap;
5859
pub use bitmap::parse_bitmap;
5960
pub use decimal::display_decimal_128;

src/query/functions/src/scalars/bitmap.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use databend_common_expression::with_unsigned_integer_mapped_type;
3939
use databend_common_io::HybridBitmap;
4040
use databend_common_io::bitmap::bitmap_contains;
4141
use databend_common_io::bitmap::bitmap_len;
42+
use databend_common_io::bitmap::bitmap_min;
4243
use databend_common_io::deserialize_bitmap;
4344
use databend_common_io::parse_bitmap;
4445
use itertools::join;
@@ -481,14 +482,12 @@ pub fn register(registry: &mut FunctionRegistry) {
481482
builder.push(0);
482483
return;
483484
}
484-
let val = match deserialize_bitmap(b) {
485-
Ok(rb) => match rb.min() {
486-
Some(val) => val,
487-
None => {
488-
ctx.set_error(builder.len(), "The bitmap is empty");
489-
0
490-
}
491-
},
485+
let val = match bitmap_min(b) {
486+
Ok(Some(val)) => val,
487+
Ok(None) => {
488+
ctx.set_error(builder.len(), "The bitmap is empty");
489+
0
490+
}
492491
Err(e) => {
493492
ctx.set_error(builder.len(), e.to_string());
494493
0

0 commit comments

Comments
 (0)