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
Contributor

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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
Contributor

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.01709% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ance-encoding/src/encodings/physical/bitpacking.rs 93.85% 5 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
Contributor

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 6 times, most recently from f96ca37 to 8a69471 Compare July 14, 2026 14:24
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 15, 2026
The helper takes a logical/section name, not a filesystem path: the
decoder only has an in-memory buffer and does not know its origin.
Rename and document accordingly so the API surface stops implying a
real path is being passed. Callers that have the real file path should
continue to use Error::corrupt_file.

Updates the inline-bitpacking call sites introduced in lance-format#7696.
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 15, 2026
…press

The mini-block decompressor path already short-circuits num_values == 0
to avoid decoding a missing inline bit-width header, but the block-level
decompressor path did not. After the validations added in lance-format#7696 (which
convert asserts into CorruptFile errors), decoding an empty block
through BlockDecompressor::decompress surfaced as a spurious
"too small for N-byte header" corrupt-file error instead of returning
an empty DataBlock.

Add the same num_values == 0 short-circuit to the block-level path and
share an empty_block() helper between the two entry points to avoid
duplication. Regression test covers u8/u16/u32/u64 widths.

Refs lance-format#7794.
@u70b3

u70b3 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Two follow-up commits pushed to address review feedback:

  • 92450abbaP2 fix: renamed Error::corrupt_file_at_pathcorrupt_file_named. The helper takes a logical/section name, not a filesystem path (the decoder only has an in-memory buffer), so the new name + doc stop the API from implying a real path is being passed. Callers that have the real path keep using corrupt_file. The misleading Encountered corrupt file {path}: Display wording is a pre-existing property of the shared CorruptFile variant (used by 10+ existing call sites), so it is intentionally left untouched in this PR.

  • 9298b507dP1 opportunistic fix (tracked separately in bug: BlockDecompressor::decompress returns spurious CorruptFile error for empty inline-bitpacking blocks (num_values=0) #7794): added the num_values == 0 short-circuit to BlockDecompressor::decompress. The mini-block path already short-circuited, but the block-level path did not, so decoding an empty block through BlockDecompressor::decompress surfaced as a spurious CorruptFile("too small for N-byte header") after the assert→CorruptFile conversion in this PR. This path already panicked on main, so it is not a regression — fixing it here opportunistically while the file is open, rather than leaving the two decompressor entry points asymmetric. The empty-block construction is extracted into a shared empty_block() helper to avoid duplication, and a regression test (test_inline_bitpacking_decompress_empty_block, u8/u16/u32/u64) guards both it and bug: BlockDecompressor::decompress returns spurious CorruptFile error for empty inline-bitpacking blocks (num_values=0) #7794.

Verified: cargo check --tests --benches, cargo test -p lance-encoding --lib bitpacking (now 24, was 20), cargo clippy --tests -- -D warnings, and cargo fmt --all -- --check all clean.

u70b3 added a commit to u70b3/lance that referenced this pull request Jul 15, 2026
The helper takes a logical/section name, not a filesystem path: the
decoder only has an in-memory buffer and does not know its origin.
Rename and document accordingly so the API surface stops implying a
real path is being passed. Callers that have the real file path should
continue to use Error::corrupt_file.

Updates the inline-bitpacking call sites introduced in lance-format#7696.
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 15, 2026
…press

The mini-block decompressor path already short-circuits num_values == 0
to avoid decoding a missing inline bit-width header, but the block-level
decompressor path did not. After the validations added in lance-format#7696 (which
convert asserts into CorruptFile errors), decoding an empty block
through BlockDecompressor::decompress surfaced as a spurious
"too small for N-byte header" corrupt-file error instead of returning
an empty DataBlock.

Add the same num_values == 0 short-circuit to the block-level path and
share an empty_block() helper between the two entry points to avoid
duplication. Regression test covers u8/u16/u32/u64 widths.

Refs lance-format#7794.
@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch 2 times, most recently from 8499699 to 40b725b Compare July 16, 2026 01:39
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 16, 2026
The helper takes a logical/section name, not a filesystem path: the
decoder only has an in-memory buffer and does not know its origin.
Rename and document accordingly so the API surface stops implying a
real path is being passed. Callers that have the real file path should
continue to use Error::corrupt_file.

Updates the inline-bitpacking call sites introduced in lance-format#7696.
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 16, 2026
…press

The mini-block decompressor path already short-circuits num_values == 0
to avoid decoding a missing inline bit-width header, but the block-level
decompressor path did not. After the validations added in lance-format#7696 (which
convert asserts into CorruptFile errors), decoding an empty block
through BlockDecompressor::decompress surfaced as a spurious
"too small for N-byte header" corrupt-file error instead of returning
an empty DataBlock.

Add the same num_values == 0 short-circuit to the block-level path and
share an empty_block() helper between the two entry points to avoid
duplication. Regression test covers u8/u16/u32/u64 widths.

Refs lance-format#7794.
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 17, 2026
The helper takes a logical/section name, not a filesystem path: the
decoder only has an in-memory buffer and does not know its origin.
Rename and document accordingly so the API surface stops implying a
real path is being passed. Callers that have the real file path should
continue to use Error::corrupt_file.

Updates the inline-bitpacking call sites introduced in lance-format#7696.
u70b3 added a commit to u70b3/lance that referenced this pull request Jul 17, 2026
…press

The mini-block decompressor path already short-circuits num_values == 0
to avoid decoding a missing inline bit-width header, but the block-level
decompressor path did not. After the validations added in lance-format#7696 (which
convert asserts into CorruptFile errors), decoding an empty block
through BlockDecompressor::decompress surfaced as a spurious
"too small for N-byte header" corrupt-file error instead of returning
an empty DataBlock.

Add the same num_values == 0 short-circuit to the block-level path and
share an empty_block() helper between the two entry points to avoid
duplication. Regression test covers u8/u16/u32/u64 widths.

Refs lance-format#7794.
@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch 5 times, most recently from 31bfa72 to 31e242a Compare July 21, 2026 05:59
@u70b3

u70b3 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Hi @hamersaw ,
This PR is ready for review.
Could you please take some time to look through it when you are available?

Thanks a lot!

@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch 2 times, most recently from dde3636 to 93f9250 Compare July 22, 2026 03:34
@Xuanwo

Xuanwo commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Hi, can you measure the end to end perf performance for this PR?

@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch from 93f9250 to 08a6dd9 Compare July 22, 2026 07:52
@u70b3

u70b3 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the suggestion. I measured this at two end-to-end scan layers:

  1. FileReader: scheduler creation, file open, projection, full-range streaming, decoding, and RecordBatch materialization.
  2. Public Dataset::scan(): projection, streaming, decoding, and materialization (with the dataset handle opened before timing, matching the existing scan benchmark style).

Workload and procedure

  • Compared main 74c0d3897 with this PR at 08a6dd9c3.
  • Used one shared Lance V2.1 fixture with 33,554,432 rows:
    • 12-bit Int32, BSS disabled
    • 23-bit UInt64, BSS disabled
    • uncompressed Float32
  • Validated from page metadata that both integer columns use InlineBitpacking.
  • Generated the fixture with main; both builds read the exact same data file (SHA-256 caa228b68515065d10831b6b9508ef456f9eaf02a32bee5fe244ba5d55c79920).
  • Used separate benchmark binaries and pinned both to CPUs 0,2,4,6,8,10.
  • Ran five counterbalanced pairs: main/PR, PR/main, main/PR, PR/main, main/PR.
  • Each case used a 2-second warm-up, at least 6 seconds of measurement, and 20 Criterion samples. No runs were excluded.
  • The confidence intervals below are paired bootstrap 95% CIs across the five runs. A negative latency delta means the PR was faster.

Results

Path / projection Main PR PR latency delta Paired 95% CI
FileReader / i32 26.110 ms 25.968 ms -0.45% [-4.43%, +2.53%]
FileReader / u64 35.078 ms 35.166 ms +0.29% [-2.02%, +2.30%]
FileReader / both bitpacked columns 57.546 ms 58.604 ms +1.89% [-0.72%, +3.80%]
FileReader / bitpacked + flat 93.403 ms 94.968 ms +1.78% [-1.72%, +5.29%]
Dataset scan / i32 29.443 ms 28.635 ms -2.69% [-5.19%, +0.06%]
Dataset scan / both bitpacked columns 57.318 ms 56.610 ms -1.11% [-4.22%, +1.99%]
Dataset scan / bitpacked + flat 90.209 ms 87.715 ms -2.69% [-5.17%, +0.30%]

All confidence intervals cross zero, so I could not establish a statistically distinguishable end-to-end improvement or regression on this machine. Run-to-run CV was roughly 1%–4%, and individual paired results changed direction.

I also temporarily instrumented a representative read to verify whether the new borrowed-buffer path was reached. The i32 case borrowed all 16,384 chunks. For u64, 5,463 chunks were borrowed and 10,921 fell back to an owned copy because of alignment. This helps explain why the improvement in the aligned unchunk microbenchmark is diluted in a full scan.

These measurements are warm local-cache scans on WSL2 (Intel i7-10700). They verify that the optimized path is exercised, but do not demonstrate a broader cold-cache or remote-object-store improvement.

@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch 2 times, most recently from 94babb0 to 6729a5f Compare July 23, 2026 09:26

@westonpace westonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This change seems fine to me. Just one question around why we are getting these empty chunks.


impl BlockDecompressor for InlineBitpacking {
fn decompress(&self, data: LanceBuffer, num_values: u64) -> Result<DataBlock> {
if num_values == 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are we getting empty chunks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We aren't, as far as I can tell — I don't know of any caller that reaches the block-level decompressor with num_values == 0. I checked by replacing the short-circuit with a panic! and running the lance-encoding test suite: the only failures were the synthetic unit tests added in this PR. Sparse round-trips, empty-batch reads, and all-null pages never hit it.

The guard is there to match the mini-block entry point, which does have a real trigger: #7752 added it because sparse pages that project to zero leaf values still decode an empty value buffer. When I converted the assert!s in unchunk into CorruptFile errors, the same empty input on the block side would have failed with a misleading "too small for N-byte header" corrupt-file error (it panicked before), so I filed #7794 and added the matching short-circuit. Note the issue text says this PR added the mini-block guard — that's wrong, it came from #7752.

It's one branch on a cold path and also covers 0-row pages and corrupt/legacy files, so I'd lean towards keeping it, but I can move it out of this PR if you prefer.

@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch from 6729a5f to 0b75ff0 Compare July 28, 2026 01:36
- Replace the unconditional input copy in InlineBitpacking::unchunk with a
  typed view borrowed over the inline bitpacking chunk words. Misaligned
  buffers still fall back to a copy, so the new path is never slower than
  the old one.
- Validate chunk headers up front (header size, word alignment, value
  count, bit width, payload size) and reject malformed inline bitpacking
  chunks with Error::CorruptFile instead of asserting.
- Add Error::corrupt_file_named so decoders that only have a logical
  section name (not a filesystem path) can report corrupt data without
  lance-encoding depending on object_store.
- Short-circuit num_values == 0 in BlockDecompressor::decompress, sharing
  an empty_block() helper with the MiniBlockDecompressor path. Refs lance-format#7794.
- Add unchunk roundtrip/corruption tests and a Criterion benchmark
  comparing the old copy path with the new typed-view path.
@u70b3
u70b3 force-pushed the perf/inline-bitpack-unchunk-zero-copy branch from 0b75ff0 to deda929 Compare July 28, 2026 14:53
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.

3 participants