Skip to content

feat: avoid deserialization in bitmap_contains/min/max/has_any/has_all - #20210

Open
harry-hao wants to merge 10 commits into
databendlabs:mainfrom
harry-hao:bitmap_avoid_deser
Open

feat: avoid deserialization in bitmap_contains/min/max/has_any/has_all#20210
harry-hao wants to merge 10 commits into
databendlabs:mainfrom
harry-hao:bitmap_avoid_deser

Conversation

@harry-hao

@harry-hao harry-hao commented Jul 27, 2026

Copy link
Copy Markdown

I hereby agree to the terms of the CLA available at: https://docs.databend.com/dev/policies/cla/

Background

Issue [#19019](#19019) identified deserialization as the bottleneck for BITMAP_INTERSECT. A series of PRs addressed this progressively:

These PRs laid the foundation for computing on serialized bytes, follow-up work left as a good first issue tracked under [#19101](#19101). This PR picks up that work.

Summary

Implement zero-deserialization fast paths for five scalar bitmap functions on the HybridBitmap::Large variant (bitmap_contains, bitmap_min, bitmap_max, bitmap_has_any, bitmap_has_all). These functions now compute results by reading RoaringFormatSpec serialized bytes directly, avoiding full deserialization.

Zero-deserialization operations

  • bitmap_contains: intersects a single-element RoaringTreemap with the serialized buffer
  • bitmap_min / bitmap_max: reads the first/last value from the first/last container
  • bitmap_has_any / bitmap_has_all: container-level merge-join between two serialized bitmaps if both are large, if one side is large then deserialize and iterate the smaller side, then probe the larger side, fallback to deserialize both side as last sort.

Fast-path

Before entering the costly intersection path, several checks reject impossible results at zero cost:

  • Empty-bitmap: e.g. bitmap_has_any(empty, _) => false
  • Range overlap: e.g. bitmap_has_any(lhs, rhs) => false if lhs.max < rhs.min || lhs.min > rhs.max
  • Range covering: e.g. bitmap_has_all(lhs, rhs) => false if lhs.min > rhs.min || lhs.max < rhs.max
  • Cardinality: e.g. bitmap_has_all(lhs, rhs) => false if lhs.len < rhs.len
  • Container-level cardinality: same as above but per container
  • Bitmap-container full-superset shortcut: e.g. if lhs is a bitmap container (cardinality = 65536), it trivially contains any rhs container.

Supporting infrastructure

  • BitmapStats: a struct holding {len, min, max} computed once from serialized bytes, reused by bitmap_has_any/bitmap_has_all for range and cardinality rejection

Tests & benchmarks

  • 6 unit tests in bitmap.rs: test_bitmap_contains, test_bitmap_min, test_bitmap_max, test_bitmap_has_any, test_bitmap_stats, test_bitmap_has_all -- covering HybridLarge, HybridSmall, Legacy, and empty-bitmap cases.
  • 16 divan benchmarks for the optimized functions in the bitmap_scalar group: bitmap_contains (large + small), bitmap_min (large + small), bitmap_max (large + small), bitmap_has_any (5 variants including disjoint), bitmap_has_all (5 variants including disjoint).

Performance

Benchmarks collected from bench (cargo bench -p databend-common-functions --bench bench -- bitmap_scalar), taking median values for compare:

Function Baseline Median PR Median Faster
bitmap_contains_large 13.7 us 2.0 us 7x
bitmap_min_large 11.4 us 0.55 us 20x
bitmap_max_large 11.5 us 0.53 us 22x
bitmap_has_all_large_large 40.5 us 10.4 us 4x
bitmap_has_any_large_large 27.5 us 2.7 us 10x

Remaining work

Tests

  • Unit Test
  • Logic Test
  • Benchmark Test
  • No Test - Explain why

Type of change

  • Bug Fix (non-breaking change which fixes an issue)
  • New Feature (non-breaking change which adds functionality)
  • Breaking Change (fix or feature that could cause existing functionality not to work as expected)
  • Documentation Update
  • Refactoring
  • Performance Improvement
  • Other (please describe):

This change is Reviewable

@harry-hao harry-hao changed the title perf: avoid deserialization in bitmap_contains/min/max/has_any/has_all feat: avoid deserialization in bitmap_contains/min/max/has_any/has_all Jul 27, 2026
@github-actions github-actions Bot added the pr-feature this PR introduces a new feature to the codebase label Jul 27, 2026
@harry-hao

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2aab60db4a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/common/io/src/bitmap/reader.rs Outdated
@harry-hao
harry-hao force-pushed the bitmap_avoid_deser branch from 2aab60d to 02454bf Compare July 27, 2026 13:49
@harry-hao

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 02454bf569

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@harry-hao
harry-hao marked this pull request as ready for review July 27, 2026 13:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02454bf569

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/common/io/src/bitmap.rs
@harry-hao
harry-hao force-pushed the bitmap_avoid_deser branch from 02454bf to cd08039 Compare July 27, 2026 15:00
Added a `bitmap_scalar` bench group covering:
- bitmap_contains
- bitmap_count
- bitmap_min
- bitmap_max
- bitmap_and
- bitmap_has_any
- bitmap_has_all
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).
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).
For large hybrid bitmaps, bitmap_max now reads the maximum value
directly from the serialized buffer (last element of the last
container) instead of deserializing the entire roaring treemap.

This reduces bitmap_max_large from ~10.7µs to ~531ns (20x speedup).
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).
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).
@harry-hao
harry-hao force-pushed the bitmap_avoid_deser branch from 9bafe89 to 4ba28b9 Compare July 28, 2026 02:07
@harry-hao

Copy link
Copy Markdown
Author

Hi @forsaken628, could you review this PR? It adds zero-deserialization fast paths for bitmap_contains/min/max/has_any/has_all, operating directly on serialized HybridLarge buffers via a TreemapReader instead of full deserialize + method call. The else branch (HybridSmall + Legacy) still uses deserialize_bitmap fallback. I'd appreciate your perspective on the reader.rs zero-deserialization approach and the bitmap.rs dispatch design.

@b41sh
b41sh requested a review from KKould July 28, 2026 02:28
@KKould
KKould requested a review from forsaken628 July 28, 2026 02:35
@forsaken628

Copy link
Copy Markdown
Collaborator

I do not object to reimplementing these operations instead of relying on roaring-rs, but the current implementation is too fragmented. Although TreemapReader and BitmapReader already exist, container-level behavior is still spread across many free functions operating on raw byte slices. A dedicated zero-copy serialized container type should encapsulate parsing, validation, representation dispatch, and operations such as contains, min, max, intersection, and superset checks.

The repeated treemap/container merge traversal should also be centralized, with operation-specific logic supplied through a visitor or handler. ab59ebef (RoaringBitmap/roaring-rs@ab59ebe) provides a useful reference for separating traversal from operation handlers. It is designed around roaring-rs’s materialized containers, so Databend does not need to copy it directly; since both operands here are serialized buffers, we can design a simpler zero-copy visitor around our own serialized container abstraction.

The current tests are also insufficient for maintaining an independent implementation of Roaring semantics. There are some fixed differential checks for has_any and has_all, but equivalent comparisons against RoaringTreemap should cover all newly implemented operations. Property-based tests are also needed, with structured coverage of array/bitmap combinations, multiple prefix buckets, empty inputs, and cardinality boundaries such as 4095/4096/4097.

@harry-hao

Copy link
Copy Markdown
Author

I do not object to reimplementing these operations instead of relying on roaring-rs, but the current implementation is too fragmented. Although TreemapReader and BitmapReader already exist, container-level behavior is still spread across many free functions operating on raw byte slices. A dedicated zero-copy serialized container type should encapsulate parsing, validation, representation dispatch, and operations such as contains, min, max, intersection, and superset checks.

The repeated treemap/container merge traversal should also be centralized, with operation-specific logic supplied through a visitor or handler. ab59ebef (RoaringBitmap/roaring-rs@ab59ebe) provides a useful reference for separating traversal from operation handlers. It is designed around roaring-rs’s materialized containers, so Databend does not need to copy it directly; since both operands here are serialized buffers, we can design a simpler zero-copy visitor around our own serialized container abstraction.

The current tests are also insufficient for maintaining an independent implementation of Roaring semantics. There are some fixed differential checks for has_any and has_all, but equivalent comparisons against RoaringTreemap should cover all newly implemented operations. Property-based tests are also needed, with structured coverage of array/bitmap combinations, multiple prefix buckets, empty inputs, and cardinality boundaries such as 4095/4096/4097.

Thank you for the detailed review. I fully agree with your feedback:

  1. Current implementation is indeed too fragmented. A dedicated zero-copy serialized container type should make it more maintainable.
  2. Centralizing the repeated merge traversal logic through visitor pattern is a good idea.
  3. Tests comparing against RoaringTreemap and property-based tests will be added.

I'll work on refactoring the implementation and expanding the test coverage accordingly.

Refactor the 5 bitmap operation functions to improve
maintainability:

- bitmap_contains
- bitmap_min
- bitmap_max
- bitmap_has_any
- bitmap_has_all

Key changes:

- ContainerReader: read and operate on a roaring container
- SmallReader: same as ContainerReader but for HybridBitmap::Small
- ContainerVisitor: merge traversal of two serialized treemaps
- ContainerHandler: trait abstract container handle logic, used by ContainerVisitor
- HasAnyHandler/HasAllHandler: impl ContainerHandler for bitmap_has_any/all
- Tests restructured to compare against RoaringTreemap as ground truth, sharing fixtures across all tests, covering combinations of: format, container type, set relationship, and key
  alignment.
This commit introduced [proptests](https://docs.rs/proptest) for newly introduced:

- bitmap_contains
- bitmap_min
- bitmap_max
- bitmap_has_any
- bitmap_has_all

Each compares the result against RoaringTreemap as ground truth.

Strategy design inspired by roaring as:

- Store (random bits)
- Container (50% Array, 50% Bitmap)
- Bitmap (0 to 16 containers)
- Tree (0 to 16 Bitmaps)
- Serialization (80% Hybrid, 10% Legacy, 10% Empty)
@harry-hao
harry-hao force-pushed the bitmap_avoid_deser branch from 6369790 to 4ae69c5 Compare July 30, 2026 10:05
@harry-hao

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ae69c5513

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/common/io/src/bitmap.rs Outdated
… Small

Previously, when bitmap_has_all got a Small lhs and a Large rhs, it
simply return false, since a set with fewer cardinality can't has_all
another one with higher cardinality. But due to we don't always demote
Large to Small after cardinality reducing operations, that's not true
anymore.

As a defense, we removed this branch, and the doubting rhs will fall
into branch below, where rhs cardinality is actually checked before
returning false.
@harry-hao
harry-hao force-pushed the bitmap_avoid_deser branch from 4651515 to 2cf4a7e Compare July 30, 2026 10:51
@github-actions

Copy link
Copy Markdown
Contributor

🤖 CI Job Analysis (Retry 1)

Workflow: 30532109004

📊 Summary

  • Total Jobs: 31
  • Failed Jobs: 2
  • Retryable: 0
  • Code Issues: 2

NO RETRY NEEDED

All failures appear to be code/test issues requiring manual fixes.

🔍 Job Details

  • mac_check: Not retryable (Code/Test)
  • linux / check: Not retryable (Code/Test)

🤖 About

Automated analysis using job annotations to distinguish infrastructure issues (auto-retried) from code/test issues (manual fixes needed).

@harry-hao

Copy link
Copy Markdown
Author

That was a make lint error. Resolved.

@harry-hao

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cf4a7e834

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/common/io/src/bitmap.rs Outdated
if as_roaring(lhs).is_none() && as_roaring(rhs).is_some() {
let lhs_small = SmallReader::new(lhs)?;
// Fast path: rhs has more values than lhs, impossible to contain
if reader::bitmap_len_above(rhs, lhs_small.len())? {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the Roaring payload to the cardinality reader

The follow-up still passes the full Hybrid buffer here, including the HB header, but reader::bitmap_len_above expects a RoaringTreemap payload like the slice returned by as_roaring(rhs). In the HybridSmall-lhs/HybridLarge-rhs path, common inputs such as bitmap_has_all(build_bitmap(0..31), build_bitmap(0..50000)) start decoding from the header bytes and return a BadBytes error before the fallback deserialization, instead of returning false (or checking a small HybridLarge rhs accurately). Use the bound Roaring slice for this length check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved.

Comment thread src/common/io/src/bitmap.rs Outdated
fn new(buf: &'a [u8]) -> Result<Self> {
let payload = &buf[HYBRID_HEADER_LEN..];
let (len, bytes) = decode_small_payload(payload)?;
Ok(Self { len, values: bytes })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize HybridSmall payloads before fast-path reads

This reader keeps the raw HybridSmall payload order, while deserialize_bitmap accepts the same payload by inserting each value into a sorted, de-duplicated small set. For persisted HybridSmall bytes with values like [5, 1] or duplicates, the new bitmap_min/bitmap_max/bitmap_contains/set predicates now use first/last/binary-search/two-pointer logic on non-canonical bytes and can return results that differ from the previous deserialize-based implementation. Either validate sorted unique payloads before these fast paths or decode through the same normalization used by decode_small_values.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved.

…ice to bitmap_len_above

SmallReader previously used decode_small_payload which returns raw
that may contain duplicates or be unsorted. Switch to decode_small_values to
deduplicate and sort, matching the deserialize path.

Pass the roaring slice correctly to reader::bitmap_len_above. The
previous code passed the HybridLarge buffer with its HB header, causing a
BadBytes error.
@harry-hao

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70fd795f4c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if is_hybrid_large(buf) {
Some(&buf[HYBRID_HEADER_LEN..])
} else if !is_hybrid(buf) {
Some(buf) // Legacy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept alternate Roaring legacy encodings

When this legacy path sends every non-Hybrid buffer into reader::*, valid RoaringTreemap payloads encoded with the standard SERIAL_COOKIE form now fail because BitmapReader::decode rejects that cookie as does not support run containers. These legacy bytes were previously accepted by deserialize_bitmap, so bitmap_contains/bitmap_min/bitmap_max/set predicates regress for imported or persisted standard Roaring data; fall back to deserialization or add support for that cookie before taking this path.

Useful? React with 👍 / 👎.

Comment on lines +831 to +832
/// Different from [`validate_serialized_bitmap`], this one does not check payload to be
/// lightweight.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate large payloads before fast-path reads

Only checking the 8-byte treemap prefix lets non-empty Large/legacy buffers with truncated payloads bypass the old deserialize_bitmap validation: for example, bytes equal to 1u64.to_le_bytes() declare one bucket but have no bucket data, yet the new readers treat them as empty, so bitmap_has_all(valid, corrupt_rhs) returns true and bitmap_min(corrupt) reports an empty bitmap instead of BadBytes. Please validate the Roaring payload or make the reader consume the declared bucket count before returning fast-path results.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-feature this PR introduces a new feature to the codebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants