Skip to content

Commit 5c13203

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 934d56f commit 5c13203

4 files changed

Lines changed: 180 additions & 16 deletions

File tree

src/common/io/src/bitmap.rs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -728,23 +728,38 @@ 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+
pub fn bitmap_contains(buf: &[u8], value: u64) -> Result<bool> {
747+
if buf.is_empty() {
748+
return Ok(false);
749+
}
750+
if is_hybrid_large(buf) {
751+
Ok(reader::bitmap_contains(&buf[HYBRID_HEADER_LEN..], value)?)
752+
} else {
753+
Ok(deserialize_bitmap(buf)?.contains(value))
754+
}
755+
}
756+
742757
fn parse_bitmap_rhs(buf: &[u8]) -> Result<BitmapRhsView<'_>> {
743758
if buf.is_empty() {
744759
return Ok(BitmapRhsView::Empty);
745760
}
746761

747-
if buf.len() >= HYBRID_HEADER_LEN && buf[..2] == HYBRID_MAGIC && buf[2] == HYBRID_VERSION {
762+
if is_hybrid(buf) {
748763
let payload = &buf[HYBRID_HEADER_LEN..];
749764
match buf[3] {
750765
HYBRID_KIND_SMALL => {
@@ -768,7 +783,7 @@ fn validate_serialized_bitmap(buf: &[u8]) -> Result<()> {
768783
return Ok(());
769784
}
770785

771-
if buf.len() >= HYBRID_HEADER_LEN && buf[..2] == HYBRID_MAGIC && buf[2] == HYBRID_VERSION {
786+
if is_hybrid(buf) {
772787
let payload = &buf[HYBRID_HEADER_LEN..];
773788
match buf[3] {
774789
HYBRID_KIND_SMALL => {
@@ -1429,4 +1444,50 @@ mod tests {
14291444
let decoded = deserialize_bitmap(&legacy).unwrap();
14301445
assert_eq!(decoded.into_iter().collect::<Vec<_>>(), vec![1, 5, 42]);
14311446
}
1447+
1448+
#[test]
1449+
fn test_bitmap_contains() {
1450+
// HybridLarge: hit and miss (spanning two prefix buckets)
1451+
let large = HybridBitmap::from_iter(
1452+
[0u64, 2500, 65535, 65536, 65536 + 2500, 131071]
1453+
.into_iter()
1454+
.chain((0..40000).map(|v| v + 200000)),
1455+
);
1456+
let mut buf = Vec::new();
1457+
large.serialize_into(&mut buf).unwrap();
1458+
assert!(bitmap_contains(&buf, 0).unwrap());
1459+
assert!(bitmap_contains(&buf, 65536 + 2500).unwrap());
1460+
assert!(bitmap_contains(&buf, 200000).unwrap());
1461+
// 999999: no matching prefix bucket (fast-path miss)
1462+
assert!(!bitmap_contains(&buf, 999999).unwrap());
1463+
// 100000: prefix bucket exists but value not in container (search miss)
1464+
assert!(!bitmap_contains(&buf, 100000).unwrap());
1465+
1466+
// HybridLarge with array container at max capacity (cardinality = 4096)
1467+
// Tests the ARRAY_LIMIT boundary: <= 4096 is array, > 4096 is bitmap.
1468+
let boundary = HybridBitmap::from_iter(0u64..4096);
1469+
let mut buf = Vec::new();
1470+
boundary.serialize_into(&mut buf).unwrap();
1471+
assert!(bitmap_contains(&buf, 0).unwrap());
1472+
assert!(bitmap_contains(&buf, 4095).unwrap());
1473+
assert!(!bitmap_contains(&buf, 4096).unwrap());
1474+
1475+
// HybridSmall: hit and miss
1476+
let small = HybridBitmap::from_iter(0u64..31);
1477+
let mut buf = Vec::new();
1478+
small.serialize_into(&mut buf).unwrap();
1479+
assert!(bitmap_contains(&buf, 15).unwrap());
1480+
assert!(!bitmap_contains(&buf, 50).unwrap());
1481+
1482+
// Legacy: hit and miss
1483+
let mut tree = RoaringTreemap::new();
1484+
tree.insert(42);
1485+
let mut buf = Vec::new();
1486+
tree.serialize_into(&mut buf).unwrap();
1487+
assert!(bitmap_contains(&buf, 42).unwrap());
1488+
assert!(!bitmap_contains(&buf, 99).unwrap());
1489+
1490+
// Empty buffer
1491+
assert!(!bitmap_contains(&[], 42).unwrap());
1492+
}
14321493
}

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)