Skip to content

Commit 4f90c09

Browse files
committed
perf: add fast-path bitmap_contains that avoids full deserialization
For large hybrid bitmaps, bitmap_contains now reads the serialized buffer directly via binary search on container descriptions instead of deserializing the entire bitmap into memory. This reduces bitmap_contains_large from ~12.1µs to ~2.1µs (6x speedup).
1 parent 5c8794a commit 4f90c09

4 files changed

Lines changed: 218 additions & 16 deletions

File tree

src/common/io/src/bitmap.rs

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -728,23 +728,76 @@ pub fn bitmap_len(buf: &[u8]) -> Result<u64> {
728728
return Ok(0);
729729
}
730730

731-
if buf.len() > 3
732-
&& buf[3] == HYBRID_KIND_LARGE
733-
&& buf[..2] == HYBRID_MAGIC
734-
&& buf[2] == HYBRID_VERSION
735-
{
731+
if is_hybrid_large(buf) {
736732
Ok(reader::bitmap_len(&buf[HYBRID_HEADER_LEN..])? as u64)
737733
} else {
738734
Ok(deserialize_bitmap(buf)?.len())
739735
}
740736
}
741737

738+
fn is_hybrid(buf: &[u8]) -> bool {
739+
buf.len() >= HYBRID_HEADER_LEN && buf[..2] == HYBRID_MAGIC && buf[2] == HYBRID_VERSION
740+
}
741+
742+
fn is_hybrid_large(buf: &[u8]) -> bool {
743+
is_hybrid(buf) && buf[3] == HYBRID_KIND_LARGE
744+
}
745+
746+
/// Validate that a serialized bitmap buffer has minimum length.
747+
///
748+
/// Different from [`validate_serialized_bitmap`], this one does not check payload to be
749+
/// lightweight.
750+
pub(crate) fn validate_serialized_bitmap_header_and_length(buf: &[u8]) -> Result<()> {
751+
if buf.is_empty() {
752+
return Ok(());
753+
} else if is_hybrid(buf) {
754+
match buf[3] {
755+
HYBRID_KIND_SMALL => {
756+
// HybridSmall payload: at least 1 byte (length byte)
757+
if buf.len() < HYBRID_HEADER_LEN + 1 {
758+
return Err(ErrorCode::BadBytes("hybrid small bitmap: buffer too short"));
759+
}
760+
}
761+
HYBRID_KIND_LARGE => {
762+
// HybridLarge payload: at least 8 bytes (u64 prefix bucket count)
763+
if buf.len() < HYBRID_HEADER_LEN + 8 {
764+
return Err(ErrorCode::BadBytes("hybrid large bitmap: buffer too short"));
765+
}
766+
}
767+
kind => {
768+
return Err(ErrorCode::BadBytes(format!(
769+
"hybrid bitmap: invalid kind {kind}"
770+
)));
771+
}
772+
}
773+
} else if buf.len() < 8 {
774+
// Legacy RoaringTreemap: minimum 8 bytes (u64 prefix bucket count)
775+
return Err(ErrorCode::BadBytes("bitmap: buffer too short"));
776+
}
777+
778+
Ok(())
779+
}
780+
781+
pub fn bitmap_contains(buf: &[u8], value: u64) -> Result<bool> {
782+
if buf.is_empty() {
783+
return Ok(false);
784+
}
785+
786+
validate_serialized_bitmap_header_and_length(buf)?;
787+
788+
if is_hybrid_large(buf) {
789+
Ok(reader::bitmap_contains(&buf[HYBRID_HEADER_LEN..], value)?)
790+
} else {
791+
Ok(deserialize_bitmap(buf)?.contains(value))
792+
}
793+
}
794+
742795
fn parse_bitmap_rhs(buf: &[u8]) -> Result<BitmapRhsView<'_>> {
743796
if buf.is_empty() {
744797
return Ok(BitmapRhsView::Empty);
745798
}
746799

747-
if buf.len() >= HYBRID_HEADER_LEN && buf[..2] == HYBRID_MAGIC && buf[2] == HYBRID_VERSION {
800+
if is_hybrid(buf) {
748801
let payload = &buf[HYBRID_HEADER_LEN..];
749802
match buf[3] {
750803
HYBRID_KIND_SMALL => {
@@ -768,7 +821,7 @@ fn validate_serialized_bitmap(buf: &[u8]) -> Result<()> {
768821
return Ok(());
769822
}
770823

771-
if buf.len() >= HYBRID_HEADER_LEN && buf[..2] == HYBRID_MAGIC && buf[2] == HYBRID_VERSION {
824+
if is_hybrid(buf) {
772825
let payload = &buf[HYBRID_HEADER_LEN..];
773826
match buf[3] {
774827
HYBRID_KIND_SMALL => {
@@ -1429,4 +1482,50 @@ mod tests {
14291482
let decoded = deserialize_bitmap(&legacy).unwrap();
14301483
assert_eq!(decoded.into_iter().collect::<Vec<_>>(), vec![1, 5, 42]);
14311484
}
1485+
1486+
#[test]
1487+
fn test_bitmap_contains() {
1488+
// HybridLarge: hit and miss (spanning two prefix buckets)
1489+
let large = HybridBitmap::from_iter(
1490+
[0u64, 2500, 65535, 65536, 65536 + 2500, 131071]
1491+
.into_iter()
1492+
.chain((0..40000).map(|v| v + 200000)),
1493+
);
1494+
let mut buf = Vec::new();
1495+
large.serialize_into(&mut buf).unwrap();
1496+
assert!(bitmap_contains(&buf, 0).unwrap());
1497+
assert!(bitmap_contains(&buf, 65536 + 2500).unwrap());
1498+
assert!(bitmap_contains(&buf, 200000).unwrap());
1499+
// 999999: no matching prefix bucket (fast-path miss)
1500+
assert!(!bitmap_contains(&buf, 999999).unwrap());
1501+
// 100000: prefix bucket exists but value not in container (search miss)
1502+
assert!(!bitmap_contains(&buf, 100000).unwrap());
1503+
1504+
// HybridLarge with array container at max capacity (cardinality = 4096)
1505+
// Tests the ARRAY_LIMIT boundary: <= 4096 is array, > 4096 is bitmap.
1506+
let boundary = HybridBitmap::from_iter(0u64..4096);
1507+
let mut buf = Vec::new();
1508+
boundary.serialize_into(&mut buf).unwrap();
1509+
assert!(bitmap_contains(&buf, 0).unwrap());
1510+
assert!(bitmap_contains(&buf, 4095).unwrap());
1511+
assert!(!bitmap_contains(&buf, 4096).unwrap());
1512+
1513+
// HybridSmall: hit and miss
1514+
let small = HybridBitmap::from_iter(0u64..31);
1515+
let mut buf = Vec::new();
1516+
small.serialize_into(&mut buf).unwrap();
1517+
assert!(bitmap_contains(&buf, 15).unwrap());
1518+
assert!(!bitmap_contains(&buf, 50).unwrap());
1519+
1520+
// Legacy: hit and miss
1521+
let mut tree = RoaringTreemap::new();
1522+
tree.insert(42);
1523+
let mut buf = Vec::new();
1524+
tree.serialize_into(&mut buf).unwrap();
1525+
assert!(bitmap_contains(&buf, 42).unwrap());
1526+
assert!(!bitmap_contains(&buf, 99).unwrap());
1527+
1528+
// Empty buffer
1529+
assert!(!bitmap_contains(&[], 42).unwrap());
1530+
}
14321531
}

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

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ use roaring::RoaringTreemap;
2626
const DESCRIPTION_BYTES: usize = 4;
2727
const OFFSET_BYTES: usize = 4;
2828

29+
// Container layout constants
30+
const ARRAY_LIMIT: usize = 4096;
31+
const WORD_BITS: usize = 64;
32+
const WORD_BYTES: usize = 8;
33+
const BITMAP_WORDS: usize = 1024;
34+
const BITMAP_BYTES: usize = WORD_BYTES * BITMAP_WORDS;
35+
2936
#[derive(Clone)]
3037
pub struct TreemapReader<'a> {
3138
buf: &'a [u8],
@@ -113,15 +120,12 @@ impl BitmapReader<'_> {
113120
reader.seek_relative(last_container * OFFSET_BYTES as i64)?;
114121
let last_offset = reader.read_u32::<LittleEndian>()?;
115122

116-
const ARRAY_LIMIT: usize = 4096;
117-
const BITMAP_LENGTH: usize = 8192;
118-
119123
let size = 4
120124
+ last_offset as usize
121-
+ if last_cardinality < ARRAY_LIMIT {
125+
+ if last_cardinality <= ARRAY_LIMIT {
122126
2 * last_cardinality
123127
} else {
124-
BITMAP_LENGTH
128+
BITMAP_BYTES
125129
};
126130

127131
if buf.len() < size {
@@ -142,7 +146,6 @@ impl BitmapReader<'_> {
142146
self.containers as usize
143147
}
144148

145-
#[allow(dead_code)]
146149
pub fn prefix(&self) -> u32 {
147150
self.prefix
148151
}
@@ -164,6 +167,26 @@ impl BitmapReader<'_> {
164167
pub fn bitmap_buf(&self) -> &[u8] {
165168
&self.buf[4..]
166169
}
170+
171+
pub(crate) fn container_offset(&self, i: usize) -> io::Result<usize> {
172+
if i >= self.containers() {
173+
return Err(Error::other("index out of range"));
174+
}
175+
let offset_table_start = 12 + self.containers() * DESCRIPTION_BYTES;
176+
let offset_pos = offset_table_start + i * OFFSET_BYTES;
177+
if offset_pos + OFFSET_BYTES > self.buf.len() {
178+
return Err(Error::other("offset table too short"));
179+
}
180+
let mut reader = Cursor::new(&self.buf[offset_pos..]);
181+
let offset = reader.read_u32::<LittleEndian>()? as usize;
182+
if offset > self.bitmap_buf().len() {
183+
return Err(Error::new(
184+
ErrorKind::InvalidData,
185+
"container offset exceeds bitmap data",
186+
));
187+
}
188+
Ok(offset)
189+
}
167190
}
168191

169192
pub struct Description {
@@ -190,6 +213,84 @@ pub fn bitmap_len(buf: &[u8]) -> io::Result<usize> {
190213
Ok(sum)
191214
}
192215

216+
pub(crate) fn bitmap_contains(buf: &[u8], value: u64) -> io::Result<bool> {
217+
let tree = TreemapReader::new(buf)?;
218+
let prefix = (value >> 32) as u32;
219+
let container_key = ((value >> 16) & 0xFFFF) as u16;
220+
let low16 = (value & 0xFFFF) as u16;
221+
222+
for bitmap_result in tree.iter() {
223+
let bitmap = bitmap_result?;
224+
if bitmap.prefix() < prefix {
225+
continue;
226+
}
227+
if bitmap.prefix() > prefix {
228+
return Ok(false);
229+
}
230+
return bitmap_contains_in_bitmap(&bitmap, container_key, low16);
231+
}
232+
Ok(false)
233+
}
234+
235+
fn bitmap_contains_in_bitmap(
236+
bitmap: &BitmapReader<'_>,
237+
container_key: u16,
238+
low16: u16,
239+
) -> io::Result<bool> {
240+
let mut lo = 0;
241+
let mut hi = bitmap.containers();
242+
while lo < hi {
243+
let mid = lo + (hi - lo) / 2;
244+
let desc = bitmap.description(mid)?;
245+
match desc.prefix.cmp(&container_key) {
246+
std::cmp::Ordering::Less => lo = mid + 1,
247+
std::cmp::Ordering::Greater => hi = mid,
248+
std::cmp::Ordering::Equal => {
249+
let cardinality = desc.cardinality();
250+
let offset = bitmap.container_offset(mid)?;
251+
let container_data = &bitmap.bitmap_buf()[offset..];
252+
return if cardinality <= ARRAY_LIMIT {
253+
array_container_contains(container_data, cardinality, low16)
254+
} else {
255+
bitmap_container_contains(container_data, low16)
256+
};
257+
}
258+
}
259+
}
260+
Ok(false)
261+
}
262+
263+
/// Binary search in an array container (cardinality < 4096).
264+
fn array_container_contains(data: &[u8], cardinality: usize, low16: u16) -> io::Result<bool> {
265+
if data.len() < cardinality * 2 {
266+
return Err(Error::other("array container too short"));
267+
}
268+
let mut lo = 0;
269+
let mut hi = cardinality;
270+
while lo < hi {
271+
let mid = lo + (hi - lo) / 2;
272+
let v = u16::from_le_bytes(data[mid * 2..mid * 2 + 2].try_into().unwrap());
273+
match v.cmp(&low16) {
274+
std::cmp::Ordering::Less => lo = mid + 1,
275+
std::cmp::Ordering::Greater => hi = mid,
276+
std::cmp::Ordering::Equal => return Ok(true),
277+
}
278+
}
279+
Ok(false)
280+
}
281+
282+
/// Bit lookup in a bitmap container (cardinality >= 4096).
283+
fn bitmap_container_contains(data: &[u8], low16: u16) -> io::Result<bool> {
284+
if data.len() < BITMAP_BYTES {
285+
return Err(Error::other("bitmap container too short"));
286+
}
287+
let word_index = low16 as usize / WORD_BITS;
288+
let bit_index = low16 as usize % WORD_BITS;
289+
let start = word_index * WORD_BYTES;
290+
let word = u64::from_le_bytes(data[start..start + WORD_BYTES].try_into().unwrap());
291+
Ok(word & (1 << bit_index) != 0)
292+
}
293+
193294
pub fn intersection_with_serialized(tree: &mut RoaringTreemap, buf: &[u8]) -> io::Result<()> {
194295
use std::cmp::Ordering::*;
195296
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
@@ -53,6 +53,7 @@ pub use bitmap::HYBRID_MAGIC;
5353
pub use bitmap::HYBRID_VERSION;
5454
pub use bitmap::HybridBitmap;
5555
pub use bitmap::LARGE_THRESHOLD;
56+
pub use bitmap::bitmap_contains;
5657
pub use bitmap::deserialize_bitmap;
5758
pub use bitmap::parse_bitmap;
5859
pub use decimal::display_decimal_128;

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use databend_common_expression::vectorize_with_builder_3_arg;
3737
use databend_common_expression::with_signed_integer_mapped_type;
3838
use databend_common_expression::with_unsigned_integer_mapped_type;
3939
use databend_common_io::HybridBitmap;
40+
use databend_common_io::bitmap::bitmap_contains;
4041
use databend_common_io::bitmap::bitmap_len;
4142
use databend_common_io::deserialize_bitmap;
4243
use databend_common_io::parse_bitmap;
@@ -269,9 +270,9 @@ pub fn register(registry: &mut FunctionRegistry) {
269270
builder.push(false);
270271
return;
271272
}
272-
match deserialize_bitmap(b) {
273-
Ok(rb) => {
274-
builder.push(rb.contains(item));
273+
match bitmap_contains(b, item) {
274+
Ok(found) => {
275+
builder.push(found);
275276
}
276277
Err(e) => {
277278
ctx.set_error(builder.len(), e.to_string());

0 commit comments

Comments
 (0)