Skip to content

Optimize SearchSorted to reuse probes and avoid repeated context creation - #9904

Draft
joseph-isaacs wants to merge 4 commits into
developfrom
ji/patches-chunk-offset-probe
Draft

joseph-isaacs wants to merge 4 commits into
developfrom
ji/patches-chunk-offset-probe

Conversation

@joseph-isaacs

Copy link
Copy Markdown
Contributor

Summary

This change optimizes the SearchSorted implementations to reuse a single RepeatedArrayProbe and execution context across all comparisons in a search, rather than creating a new probe and context for each element access. This significantly reduces overhead for searches over encoded arrays.

Additionally, a new SearchSortedArray adapter is introduced to support searching over arrays of any encoding using Scalar comparisons, with the same probe-reuse optimization.

Changes

SearchSortedPrimitiveArray Optimization

  • Refactored SearchSortedPrimitiveArray from a tuple struct to a struct with named fields (reader, len, ctx, _ptype)
  • Introduced a Reader<'a, T> enum that distinguishes between two cases:
    • Values(&'a [T]): Direct buffer access for canonical, non-nullable, host-backed arrays (zero-copy fast path)
    • Probe(RefCell<RepeatedArrayProbe>): Probe-based access for all other cases
  • Added reader() method to determine which access pattern to use based on array properties
  • Added typed_value() method that returns Option<T> to distinguish nulls from zero values
  • Simplified value() method to use typed_value() and map nulls to T::zero()
  • Updated IndexOrd<Option<T>> implementation to use a single typed_value() call instead of separate validity and value checks
  • All field accesses now use the struct fields instead of tuple indexing

New SearchSortedArray Implementation

  • Created new scalar.rs module with SearchSortedArray struct for searching any array type using Scalar comparisons
  • Implements IndexOrd<Scalar> with probe reuse across the entire search
  • Includes tests demonstrating scalar search with and without nulls

Module Organization

  • Exported SearchSortedArray from search_sorted/mod.rs
  • Removed the generic IndexOrd<Scalar> implementation for ArrayRef (now handled by SearchSortedArray)

Patches Optimization

  • Refactored search_index_chunked() to reuse a single probe and context for three reads of chunk_offsets, rather than creating a new pair per read
  • Added #[allow(clippy::disallowed_methods)] annotation to document the intentional use of legacy_session()

Test Coverage

  • Added search_sorted_reads_canonical_values_directly() test verifying the fast path for canonical arrays
  • Added search_sorted_probes_when_values_are_not_addressable() test verifying probe-based access for nullable arrays
  • Added search_sorted_scalar() and search_sorted_scalar_with_nulls() tests for the new SearchSortedArray
  • Updated fuzz tests to pass execution context to assert_search_sorted()

API Changes

The SearchSortedPrimitiveArray struct layout changed from a tuple struct to a named-field struct. This is a breaking change for any code that directly constructs or pattern-matches on this type, though it is primarily used through the SearchSorted trait.

A new public type SearchSortedArray is introduced for searching arrays with Scalar comparisons.

https://claude.ai/code/session_01UPw4v7SA2A3Ab5t9zwGbRV

joseph-isaacs and others added 4 commits September 16, 2026 09:58
`SearchSortedPrimitiveArray` held a bare `&ArrayRef` and did a one-off
`execute_scalar` per comparison, so every probe in a binary search rebuilt
the state its predecessor had just thrown away. For a nullable array that
meant resolving `Validity` from the encoding on each read, and
`IndexOrd<Option<T>>` paid for it twice: once in its own `is_valid` call and
again inside `execute_scalar`, which checks validity before dispatching.

Hold a `RepeatedArrayProbe` instead. Validity is resolved on the first
comparison and reused by the remaining ~log2(n), as is any state the
encoding keeps. A null element reads back as a null scalar, so the separate
`is_valid` call is redundant: `IndexOrd<Option<T>>` now decides from the one
read, halving the probes on the nullable path.

The array is no longer borrowed for the searcher's lifetime, since the probe
owns a handle to it; only the execution context is.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPw4v7SA2A3Ab5t9zwGbRV
…rray

`Patches::search_index` already sidesteps the scalar path when its indices
are a canonical primitive array, searching the buffer as a `&[T]`.
`RunEnd::find_physical_index` has no such path, so a run-end array whose
ends are plain `u32`s — which is what they are after a file read — still
pays a scalar read per probe.

Put the fast path in the searcher instead of in each caller, so every
`SearchSortedPrimitiveArray` user gets it. The buffer can only be read as
`[T]` when the array is canonical, is host-backed, and is non-nullable,
since a null element leaves an arbitrary value in the buffer; everything
else keeps the probe.

The array is borrowed for the searcher's lifetime again, as the values are.

This also settles the cost the previous commit adds on non-nullable arrays,
where a retained read buys nothing while no encoding keeps state: those
arrays no longer reach the probe at all. What still does — compressed ends,
non-primitive patch indices — is where a retained probe pays off once those
encodings keep state.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPw4v7SA2A3Ab5t9zwGbRV
`IndexOrd<Scalar> for ArrayRef` built a whole execution context per
comparison — `legacy_session().create_execution_ctx()` inside `index_cmp` —
so a binary search over 65,536 elements created sixteen of them and threw
each away after one read. A trait impl on `ArrayRef` has nowhere to keep
anything, so this could not be fixed in place.

Replace it with `SearchSortedArray`, which takes the caller's context and a
`RepeatedArrayProbe`, matching `SearchSortedPrimitiveArray`. The searcher
now has the same shape whether or not the element type is known, and the
doc points at the typed one, which reads canonical values directly.

The only caller was the fuzzer, which already had a context in scope.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPw4v7SA2A3Ab5t9zwGbRV
…eads

`Patches::search_index_chunked` read three chunk offsets out of the same
array through `chunk_offset_at`, which builds an execution context and a
one-off probe per call — three of each per lookup, for three reads of one
small array.

Read them through a single `RepeatedArrayProbe` and a single context.

`chunk_offset_at` stays as the public single-read accessor.

Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPw4v7SA2A3Ab5t9zwGbRV
@codspeed

codspeed Bot commented Sep 16, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 1 regressed benchmark
✅ 2199 untouched benchmarks
⏩ 218 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime filtered_owned_i64_avx512[OneNullInEight] 22.8 µs 26.8 µs -14.81%
WallTime arrow_checked_add_u32_neon[16384] 20.4 µs 13.5 µs +51.34%
Simulation allocate_drop_bytes[0] 575.7 ns 521.6 ns +10.39%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing ji/patches-chunk-offset-probe (0ea6472) with develop (4af09a9)

Open in CodSpeed

Footnotes

  1. 218 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

// The three reads below are of the same array, so they share one probe and one context
// rather than building a pair per read as `Self::chunk_offset_at` does.
let mut probe = chunk_offsets.repeated_probe();
let mut ctx = legacy_session().create_execution_ctx();

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.

just pass the context to the method

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.

2 participants