Skip to content

perf(rowids): count RangeWithHoles holes in O(1) - #9242

Open
jackylee-ch wants to merge 1 commit into
lance-format:mainfrom
jackylee-ch:perf/rowid-hole-count-o1
Open

jackylee-ch wants to merge 1 commit into
lance-format:mainfrom
jackylee-ch:perf/rowid-hole-count-o1

Conversation

@jackylee-ch

Copy link
Copy Markdown
Contributor

U64Segment::len() computes the hole count for a RangeWithHoles segment with holes.iter().count(), which allocates a boxed dyn DoubleEndedIterator and steps it once per hole. U64Segment::get() already uses holes.len() for the same quantity — #6716 introduced that form there while fixing an O(N*K) row-id lookup, and len() was left behind. The two are equivalent unconditionally: every EncodedU64Array variant implements iter() as cloned()/map() over its backing Vec, so it yields exactly len() items.

RowIdSequence::get() calls segment.len() on every lookup. RangeWithHoles is selected while holes stay under a thirty-second of the span, so a segment covering a million slots can legitimately carry tens of thousands of them.

Testing

cargo test --release -p lance-table rowids (113 passed). test_segments already asserts values.len() == segment.len() over the RangeWithHoles cases; perturbing the arm fails it and five other tests. Also cargo fmt --all -- --check and cargo clippy --release -p lance-table --all-features --all-targets -- -D warnings.

`U64Segment::len()` computes the hole count for a `RangeWithHoles` segment with
`holes.iter().count()`, which allocates a boxed `dyn DoubleEndedIterator` and
steps it once per hole. `U64Segment::get()` already uses `holes.len()` for the
same quantity, and lance-format#6716 introduced that form there while fixing an O(N*K)
row-id lookup; `len()` was left on the counting form.

The two are equivalent unconditionally: every `EncodedU64Array` variant
implements `iter()` as `cloned()`/`map()` over its backing `Vec`, so the
iterator yields exactly `len()` items.

`RowIdSequence::get()` calls `segment.len()` on every lookup, and
`RowIdSequence::slice()` and `U64Segment::mask()` call it once and four times
per invocation respectively. `RangeWithHoles` is selected while holes stay
under a thirty-second of the span, so a segment covering a million slots can
legitimately carry tens of thousands of holes, each one costing a virtual call
per lookup.

## Testing

`cargo test --release -p lance-table rowids` (113 passed). `test_segments`
already asserts `values.len() == segment.len()` over the `RangeWithHoles`
cases; perturbing the arm fails it along with five other tests, so the line is
covered. Also `cargo fmt --all -- --check` and
`cargo clippy --release -p lance-table --all-features --all-targets -- -D warnings`.

@lance-gatekeeper lance-gatekeeper 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.

Gate recommendation: approve.

This uses EncodedU64Array::len() to preserve RangeWithHoles length semantics while removing the boxed iterator allocation and per-hole scan from repeated row-ID lookups.

@lance-gatekeeper lance-gatekeeper Bot added the K-approved Latest Gatekeeper recommendation permits acceptance. label Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

K-approved Latest Gatekeeper recommendation permits acceptance. performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant