Skip to content

Re-slice the downcast view instead of re-validating it - #57

Merged
emilk merged 2 commits into
release-0.6from
emilk/slice-fast-path
Aug 28, 2026
Merged

Re-slice the downcast view instead of re-validating it#57
emilk merged 2 commits into
release-0.6from
emilk/slice-fast-path

Conversation

@emilk

@emilk emilk commented Aug 28, 2026

Copy link
Copy Markdown
Member

Related

What

TypedArray::slice sliced the arrow array and then re-ran L::downcast on the result. For a nested type that recurses into the children, and where a child carries a validity buffer it rescans that bitmap — work the whole array was already validated against, and which slicing cannot invalidate (a slice reaches a subset of the rows).

LogicalType::slice_typed(typed, offset, length) -> Option<Self::Typed> is the new hook: the view of the sliced rows, or None to fall back to downcast. The default is None, so a hand-written LogicalType impl keeps compiling and keeps today's behavior — the hook is additive.

Every encoding in the crate overrides it:

Encoding How
leaves (primitives, strings, binary, …) the arrow array slices itself
Option<L>, newtypes, As<T, Repr> forward to the inner type
List, LargeList, ListView, Map, Dictionary, Run move the parent's window, share the child view
FixedSizeList move the child window too — arrow re-bases the values on slice
AnyList, AnyUtf8, AnyBinary dispatch per variant

Measured with the new slice bench (first commit, so the before/after is reproducible):

slice of i64 (1M)                        34ns  ->  34ns   (already cheap)
slice of Utf8 (100k)                     44ns  ->  38ns
slice of List<i64> (100k rows)           66ns  ->  51ns
slice of List<List<i64>> (100k rows)     85ns  ->  60ns
slice of List<i64>, item null buffer   1.82µs  ->  42ns   (~43x)

The last row is the case worth having: non-nullable items whose child array carries a validity buffer, so downcast scans it on every slice.

Testing

slice_agrees_with_a_full_downcast cross-checks the fast path against TypedArray::try_new(array.as_arrow().slice(…)) for 22 encodings, over five windows each (whole, prefix, suffix, one row, empty), and asserts that slicing an already-sliced array lands where slicing once does — the case that catches a fast path which restarts the window rather than composing it.

cargo test --all-features, cargo clippy --all-targets --all-features -- -D warnings, cargo fmt --check pass.

Compatibility

Additive: slice_typed has a default, so external LogicalType impls are unaffected. No public signature changes.

🤖 Generated with Claude Code

emilk and others added 2 commits August 28, 2026 12:06
Measures the cost of one `slice` call per encoding, including the case
that motivates the next commit: non-nullable list items whose child array
still carries a validity buffer, which `downcast` has to scan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`TypedArray::slice` sliced the arrow array and then re-ran `L::downcast`
on the result: for a nested type that recurses into the children, and
where a child carries a validity buffer it rescans that bitmap — work the
whole array had already been validated against, and which slicing cannot
invalidate (a slice reaches a subset of the rows).

`LogicalType::slice_typed` is the new hook: it returns the view of the
sliced rows, or `None` to fall back to `downcast`. The default is `None`,
so a hand-written `LogicalType` impl keeps compiling and keeps today's
behavior. Every encoding in the crate overrides it — the leaves slice
themselves; the offset-based composites (list, list-view, map,
dictionary, run) move the parent's window and share the child view;
fixed-size lists move the child window too, since arrow re-bases their
values on slice.

    slice of List<i64>, item null buffer   1.82µs  ->  42ns
    slice of List<List<i64>> (100k rows)     85ns  ->  60ns
    slice of i64 (1M)                        34ns  ->  34ns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@emilk
emilk marked this pull request as ready for review August 28, 2026 11:09
@emilk
emilk merged commit 2e01a57 into release-0.6 Aug 28, 2026
6 checks passed
@emilk emilk mentioned this pull request Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant