Skip to content

perf(encoding): avoid inline bitpacking input copy#7696

Open
u70b3 wants to merge 1 commit into
lance-format:mainfrom
u70b3:perf/inline-bitpack-unchunk-zero-copy
Open

perf(encoding): avoid inline bitpacking input copy#7696
u70b3 wants to merge 1 commit into
lance-format:mainfrom
u70b3:perf/inline-bitpack-unchunk-zero-copy

Conversation

@u70b3

@u70b3 u70b3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Avoid the unconditional input copy in InlineBitpacking::unchunk by borrowing a typed view over the inline bitpacking chunk words.
  • Keep output zero-initialization unchanged; this PR only changes compressed input handling.
  • Add focused unchunk roundtrip coverage and a Criterion benchmark comparing the old copy path with the new typed-view path.

Benchmark

Local machine: WSL2 on Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz, 12 logical CPUs.

Command:

cargo bench -p lance-encoding --bench decoder decode_inline_bitpacking_unchunk --features bitpacking -- --noplot

Results:

Case Path Criterion time estimate Throughput
u32_bw12_1024 legacy_copy 285.46 ns 5.0244 GiB/s
u32_bw12_1024 typed_view 247.35 ns 5.7985 GiB/s
u64_bw23_1024 legacy_copy 434.11 ns 6.3331 GiB/s
u64_bw23_1024 typed_view 340.27 ns 8.0797 GiB/s

Path-to-path deltas:

  • u32_bw12_1024: typed_view was 13.4% faster than legacy_copy
  • u64_bw23_1024: typed_view was 21.6% faster than legacy_copy

The benchmark uses aligned chunks built with LanceBuffer::reinterpret_vec, so it measures the aligned borrow_to_typed_view fast path. Misaligned buffers can still fall back to a copy.

Test Plan

  • cargo fmt --all
  • cargo test -p lance-encoding --features bitpacking
  • cargo clippy -p lance-encoding --all-features --tests --benches -- -D warnings
  • cargo clippy --all --tests --benches -- -D warnings
  • cargo bench -p lance-encoding --bench decoder decode_inline_bitpacking_unchunk --features bitpacking -- --noplot

Summary by CodeRabbit

  • New Features
    • Added an inline bitpacking decode benchmark covering additional integer and bit-width scenarios, with feature-gated coverage when supported.
  • Bug Fixes
    • Improved inline bitpacking decoding with zero-copy typed parsing and stronger upfront buffer validation.
    • Centralized corruption error handling and ensured decoded output matches expected payload details.
  • Tests
    • Expanded/updated roundtrip and corruption tests, including header sizing, alignment, payload length mismatches, invalid bit widths, and excessive value counts.
  • Chores
    • Updated crate configuration to use the shared workspace object storage dependency.

@github-actions github-actions Bot added A-encoding Encoding, IO, file reader/writer performance labels Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

InlineBitpacking::unchunk now decodes inline chunks from typed words with Pod bounds and explicit corruption validation. Decoder benchmarks compare typed-view and legacy paths for selected u32 and u64 cases.

Changes

Inline bitpacking unchunk

Layer / File(s) Summary
Typed-view unchunk decompression
rust/lance-encoding/Cargo.toml, rust/lance-encoding/src/encodings/physical/bitpacking.rs
InlineBitpacking::unchunk reads typed headers and payloads, validates layout, value counts, bit widths, and payload lengths, then returns structured corruption errors or unpacks into fixed-width output.
Roundtrip and corruption validation
rust/lance-encoding/src/encodings/physical/bitpacking.rs
Tests construct typed inline buffers and cover u32 and u64 roundtrips plus invalid headers, alignment, payload sizes, value counts, and bit widths.
Inline bitpacking benchmark
rust/lance-encoding/benches/decoder.rs
The benchmark compares legacy and typed-view decoding for u32 width 12 and u64 width 23, registering feature-enabled and disabled paths in both Criterion groups.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Benchmark
  participant InlineBitpacking
  participant BitPacking
  Benchmark->>Benchmark: build typed inline chunk
  Benchmark->>InlineBitpacking: decode typed-view chunk
  InlineBitpacking->>BitPacking: unchecked_unpack payload
  BitPacking-->>InlineBitpacking: fixed-width values
  Benchmark->>Benchmark: compare with legacy decode
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: optimizing inline bitpacking by avoiding an input copy.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-encoding/benches/decoder.rs`:
- Around line 66-67: The benchmark currently hardcodes a local bitpacking
chunk-size constant that duplicates InlineBitpacking’s chunk size. Update
decoder.rs to use the shared chunk-size value exported from lance_encoding
instead of BITPACKING_ELEMS_PER_CHUNK, and adjust the benchmark setup to
reference the existing InlineBitpacking/bitpacking constant so buffer generation
always matches the encoder’s expected chunking.

In `@rust/lance-encoding/src/encodings/physical/bitpacking.rs`:
- Around line 195-202: The inline header validation in
BitPacking::unchecked_unpack is currently using assert! on persisted input, so
replace it with an Error::corrupt_file path when the chunk size doesn’t match
the expected bit_width_value-derived length. In the bitpacking.rs decompression
flow, validate bit_width_value with checked_mul before computing the expected
byte count, then return a corrupt_file error on mismatch or overflow instead of
panicking, and only call BitPacking::unchecked_unpack after the header is safely
validated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 03b8707a-c394-46b1-a718-1a0b56c5c126

📥 Commits

Reviewing files that changed from the base of the PR and between d217a02 and 55bd610.

📒 Files selected for processing (2)
  • rust/lance-encoding/benches/decoder.rs
  • rust/lance-encoding/src/encodings/physical/bitpacking.rs

Comment thread rust/lance-encoding/benches/decoder.rs Outdated
Comment thread rust/lance-encoding/src/encodings/physical/bitpacking.rs
@github-actions github-actions Bot added A-python Python bindings A-java Java bindings + JNI A-deps Dependency updates labels Jul 9, 2026

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance-encoding/src/encodings/physical/bitpacking.rs (1)

611-649: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the error message and parametrize the corruption cases.

assert_corrupt_unchunk only matches on the CorruptFile variant, so any of the four unchunk_rejects_* tests would still pass even if a regression made it trip a different corruption branch than intended (e.g. the payload-mismatch case hitting the header/alignment guard). Assert on the message content too. The four cases also differ only by input, so they should be rstest cases with #[case::{name}(...)].

♻️ Suggested restructure
fn assert_corrupt_unchunk<T>(data: LanceBuffer, expected_msg: &str)
where
    T: ArrowNativeType + BitPacking + Pod,
{
    let err = InlineBitpacking::unchunk::<T>(data, 1).unwrap_err();
    match err {
        lance_core::Error::CorruptFile { message, .. } => {
            assert!(message.contains(expected_msg), "unexpected message: {message}");
        }
        other => panic!("expected CorruptFile, got {other:?}"),
    }
}

#[rstest]
#[case::too_small_header(LanceBuffer::from(vec![1, 2, 3]), "too small")]
#[case::misaligned_chunk_size(LanceBuffer::from(vec![0, 0, 0, 0, 0]), "multiple of")]
#[case::payload_size_mismatch(LanceBuffer::reinterpret_vec(vec![12_u32]), "payload")]
#[case::invalid_bit_width(LanceBuffer::reinterpret_vec(vec![33_u32]), "exceeds")]
fn unchunk_rejects(#[case] data: LanceBuffer, #[case] expected_msg: &str) {
    assert_corrupt_unchunk::<u32>(data, expected_msg);
}

As per coding guidelines: "Assert on both the error variant and the message content in tests; do not check only is_err()" and "Use rstest for Rust tests ... when cases differ only by inputs; use #[case::{name}(...)] for readable Rust case names".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-encoding/src/encodings/physical/bitpacking.rs` around lines 611 -
649, The `assert_corrupt_unchunk` helper and the four `unchunk_rejects_*` tests
only verify the `CorruptFile` variant, so regressions that hit the wrong
corruption branch could still pass. Update `assert_corrupt_unchunk` to also
validate the error message content, and refactor the four nearly identical
`unchunk_rejects_*` tests in `InlineBitpacking::unchunk` into a single
`rstest`-driven `unchunk_rejects` case table with readable `#[case::...]` names
and an expected message per input.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance-encoding/src/encodings/physical/bitpacking.rs`:
- Around line 611-649: The `assert_corrupt_unchunk` helper and the four
`unchunk_rejects_*` tests only verify the `CorruptFile` variant, so regressions
that hit the wrong corruption branch could still pass. Update
`assert_corrupt_unchunk` to also validate the error message content, and
refactor the four nearly identical `unchunk_rejects_*` tests in
`InlineBitpacking::unchunk` into a single `rstest`-driven `unchunk_rejects` case
table with readable `#[case::...]` names and an expected message per input.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 20218ced-df94-4c8a-8144-d770dd731da5

📥 Commits

Reviewing files that changed from the base of the PR and between 55bd610 and 5f2c2ab.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • java/lance-jni/Cargo.lock is excluded by !**/*.lock
  • python/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • rust/lance-encoding/Cargo.toml
  • rust/lance-encoding/src/encodings/physical/bitpacking.rs

@u70b3

u70b3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the outside-diff test feedback in 53c1f5cef.

The corrupt-input tests now use an rstest case table and assert both the CorruptFile variant and expected error message content, so each case verifies the intended validation branch.

Validated with:

  • cargo fmt --all
  • cargo test -p lance-encoding unchunk --features bitpacking
  • cargo clippy -p lance-encoding --all-features --tests --benches -- -D warnings

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.31818% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ance-encoding/src/encodings/physical/bitpacking.rs 94.31% 3 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@u70b3

u70b3 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Re Codecov remaining 3 Missing and 2 partials warning: I covered the reachable missing validation branch in 23027dd44 (num_values > ELEMS_PER_CHUNK).

The remaining uncovered branch is the defensive overflow arm around:

bit_width_value.checked_mul(ELEMS_PER_CHUNK as usize)

That branch is not reachable for the current InlineBitpacking::unchunk dispatch. Before this multiplication, we reject any bit_width_value > uncompressed_bit_width; uncompressed_bit_width is derived from size_of::<T>() * 8, and T is only dispatched as u8, u16, u32, or u64. So bit_width_value <= 64, and 64 * ELEMS_PER_CHUNK is 64 * 1024 = 65536, which cannot overflow usize on supported targets.

So the remaining Codecov warning is from defensive overflow handling / branch accounting, not an untested reachable corrupt-input path. Covering it would require constructing an impossible state or weakening the preceding validation.

@u70b3 u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch from 23027dd to 07e73c2 Compare July 11, 2026 06:26

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-encoding/src/encodings/physical/bitpacking.rs`:
- Line 238: In the decompression initialization within the relevant bitpacking
function, replace T::from_usize(0).unwrap() with T::default() when creating the
decompressed vector, since ArrowNativeType guarantees Default.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eb276f80-b1f2-4371-8b9f-f3b2e58bab21

📥 Commits

Reviewing files that changed from the base of the PR and between 23027dd and 07e73c2.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • java/lance-jni/Cargo.lock is excluded by !**/*.lock
  • python/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • rust/lance-encoding/Cargo.toml
  • rust/lance-encoding/benches/decoder.rs
  • rust/lance-encoding/src/encodings/physical/bitpacking.rs

Comment thread rust/lance-encoding/src/encodings/physical/bitpacking.rs Outdated
@u70b3 u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch 4 times, most recently from ffa6392 to 5c03730 Compare July 14, 2026 05:11
- Replace the unconditional input copy in InlineBitpacking::unchunk with a
  typed view borrowed over the inline bitpacking chunk words.
- Validate chunk headers up front and reject malformed inline bitpacking
  chunks with Error::CorruptFile.
- Use lance_core::Error::corrupt_file_at_path so lance-encoding does not
  need a direct object_store dependency.
- Share ELEMS_PER_CHUNK with the benchmark instead of a hard-coded
  constant.
- Keep output zero-initialization unchanged.
@u70b3 u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch from 5c03730 to f96ca37 Compare July 14, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-deps Dependency updates A-encoding Encoding, IO, file reader/writer A-java Java bindings + JNI A-python Python bindings performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant