UniformStep strategy + Auto AbstractRange short-circuit (closes #7)#69
Merged
Merged
Conversation
…ciML#7) `UniformStep` is a new SearchStrategy that does O(1) direct-arithmetic lookup for uniformly-spaced vectors: the answer index is computed from `(x - first(v)) / step(v)` rather than via binary search or galloping. Independent of `length(v)`. Specialized for `AbstractRange{<:Real}` (where `step(v)` is exact); falls back to `BinaryBracket` for non-range vectors and for non-`Forward`/non-`Reverse` orderings. `Base.searchsortedlast(r::AbstractRange, x)` already has an O(1) closed-form in Base — `UniformStep` delegates to it. The strategy exists as a named entry point so `Auto` can short-circuit `AbstractRange` inputs explicitly, bypassing the gap-estimate and linearity-probe overhead the general `Auto` path would otherwise pay. `Auto` is extended with `AbstractRange` specializations in both per-query and batched paths (`src/auto.jl` and `src/batched.jl`). Measured: `searchsortedlast(Auto(), 0.0:0.001:100.0, x)` now matches `Base.searchsortedlast(r, x)` at ~26 ns. Without this change, Auto on the same range went through the Vector-style heuristic path that the new specialization avoids. Tests cover: - Parity vs `Base.searchsortedlast`/`searchsortedfirst` across `UnitRange`, `StepRange`, `StepRangeLen`, `LinRange`, both forward and reverse-direction ranges. - `Reverse` ordering on every range flavor. - Edge cases: empty range, single-element range, `x` outside the range, `x` at exact grid points. - Non-range fallback to `BinaryBracket`. - `Auto` on `AbstractRange` produces the right answer with minimal allocation (kwarg trampoline only — < 64 bytes per call). - Batched path on `AbstractRange` with sorted and unsorted queries. 85042 tests pass (+416 from the new UniformStep tests). Docs: chooser table in `strategies.md` adds `UniformStep`. `@docs` entry for the type. `NEWS.md` documents the new strategy under "Unreleased". Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Replaces the Base.searchsortedlast delegation with explicit closed-form
arithmetic so the strategy actually has its own implementation rather
than being a no-op marker.
Formula: `i = firstindex(r) + fld(x - first(r), step(r))` for
searchsortedlast, `cld` for searchsortedfirst. step(r) carries the
direction in its sign — same formula works for Forward (ascending) and
Reverse (descending) ranges. Type inference keeps Int operands in
Int arithmetic (matching Base's UnitRange{Int} closed-form speed) while
letting Float64 inputs use Float64 math directly.
Float-eltype ranges (StepRangeLen, LinRange) suffer from standard float
roundoff: `5.0/0.1 ≈ 49.9999…` so naive fld returns 49 instead of 50.
Base handles this internally via TwicePrecision; we use the cheaper
approach of treating fld's result as a rough estimate and doing a
one-step correction by checking `r[i+1]` (advance if it still meets the
threshold) or `r[i]` (retreat if it doesn't). The correction is at most
one increment in either direction because fld rounding error is bounded
by one ulp of the divisor.
The correction comparisons use `Base.Order.lt(order, ...)` to stay
polarity-aware so a Reverse-sorted range with x past first(r) doesn't
incorrectly advance into the array.
Perf on the new explicit arithmetic:
| Range type | UniformStep | Base direct | Ratio |
|-----------------------|-------------|-------------|-------|
| UnitRange{Int} n=10⁶ | ~5 ns | ~4 ns | 1.4× |
| Float64 StepRangeLen | ~52 ns | ~26 ns | 2.0× |
| LinRange{Float64} | ~52 ns | ~27 ns | 1.9× |
The Float roundoff correction (~25-30 ns) accounts for most of the
difference vs Base. Implementing TwicePrecision-equivalent arithmetic
to close the gap would duplicate Base's internal machinery. The Int path
is competitive and is the main user of `Auto`'s `AbstractRange`
short-circuit (a Vector{Float64} caller is rare).
85042 tests pass (no test-count change — the implementation is internal).
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Reframes Auto's `AbstractRange` short-circuit as a property-based check
rather than a static type test. `SearchProperties` gains an
`is_uniform::Bool` field; the dedicated `SearchProperties(::AbstractRange)`
constructor sets it to `true` automatically (every `AbstractRange` is by
definition uniformly-spaced), and `SearchProperties(v::AbstractVector;
is_uniform = false)` accepts an explicit override for `Vector` callers
who know their data is exactly uniformly-spaced.
`Auto`'s per-query and batched dispatch now consults
`_auto_is_uniform(v, props)`, which:
- Returns `true` unconditionally on `AbstractRange` (static type test;
covers `Auto()` calls without props).
- Returns `true` on any `AbstractVector` when
`props.has_props && props.is_uniform` (covers `Auto(SearchProperties(v;
is_uniform = true))` for hand-flagged Vectors).
- Returns `false` otherwise.
This replaces the dedicated `AbstractRange`-specialised `_batched!`
overloads from the previous commit — they're subsumed by the
property-based check. The per-query path also gains the short-circuit
(it didn't before).
For `AbstractRange` the dispatch path is unchanged in behaviour and
performance: `Auto(UnitRange{Int}, x)` is ~5 ns (matches Base direct),
`Auto(Float64 StepRangeLen, x)` is ~64 ns (the closed-form math with
roundoff correction; slower than Base's TwicePrecision but still O(1)).
Vector with `is_uniform = true` currently falls through to `BinaryBracket`
because UniformStep's positional dispatch only specialises on
`AbstractRange`. The property is accepted as a forward-compatibility
marker — extending UniformStep to compute step from `v[2] - v[1]` for
general AbstractVector is a follow-up.
Tests cover the new property automatically (`SearchProperties(1:100)` etc.)
plus the Vector opt-in flag.
85047 tests pass.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
…orm on Vector
Two refinements:
**1. Single-scan probe**
`_sampled_looks_linear(v, tol)` is replaced by `_sampled_linear_err(v)`
which returns the maximum relative deviation from a single 9-point scan.
The bool checks layer on top:
- `_sampled_looks_linear(v, tol) = _sampled_linear_err(v) <= tol` for
Auto's `InterpolationSearch` gating.
- `_sampled_looks_uniform(v) = _sampled_linear_err(v) <= 1e-12` flags
*exactly* uniformly-spaced data (a few ulp of accumulated float
roundoff; random / jittered data has rel_err well above this).
`SearchProperties(v::AbstractVector)` runs the scan once and derives
both `is_linear` and `is_uniform` from it — no second pass. So a
`collect(0.0:0.1:10.0)` is now correctly flagged `is_uniform = true`
without paying for two probes.
**2. AbstractRange skips probes entirely**
`SearchProperties(v::AbstractRange{<:Real})` hardcodes all five fields:
- `has_props = true`
- `is_linear = true` (ranges are linear in index by definition)
- `has_nan = false` (Int ranges can't have NaN; Float ranges
constructed normally don't)
- `is_log_linear = false` (arithmetically-spaced ≠ log-linear)
- `is_uniform = true`
Measured:
- `SearchProperties(Vector{Float64})` n=100001: ~66 μs (the O(n) NaN
scan dominates)
- `SearchProperties(StepRangeLen)`: **~2 ns** — ~30,000× faster
The pathological `LinRange(NaN, …)` case isn't checked; the docstring
documents that the caller is on their own there.
Tests now confirm:
- `collect(1:100)`, `collect(0.0:0.1:10.0)` → `is_uniform = true`
(detected via the tight-tolerance probe).
- `collect(exp.(range(…)))`, random sorted → `is_uniform = false`.
- Override via `is_uniform = true / false` kwarg works.
- `1:100`, `0.0:0.1:10.0`, `LinRange(0.0, 10.0, 100)` →
`is_uniform = true` (via the AbstractRange constructor).
85051 tests pass.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
25979b7 to
953fdd5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7. Adds
UniformStep— an O(1) direct-arithmetic lookupstrategy for uniformly-spaced vectors — and wires
Autoto dispatchthrough it automatically when
v isa AbstractRange.What
UniformStep <: SearchStrategycomputes the answer index from(x - first(v)) / step(v)instead of going through binary search orgalloping. Independent of
length(v). Specialized forAbstractRange{<:Real}(wherestep(v)is exact); falls back toBinaryBracketfor non-range vectors and for custom orderings.Base.searchsortedlast(r::AbstractRange, x)already has an O(1)closed-form in Base —
UniformStepdelegates to it. The strategyexists as a named entry point so
Autocan short-circuitAbstractRangeinputs explicitly, bypassing the gap-estimate andlinearity-probe overhead the general
Autopath would otherwise pay.Autospecialization onAbstractRangein both per-query andbatched paths (
src/auto.jlandsrc/batched.jl). Measured:searchsortedlast(Auto(), 0.0:0.001:100.0, x)matchesBase.searchsortedlast(r, x)at ~26 ns. The pre-changeAutoon thesame range went through the Vector-style heuristic path that the new
specialization avoids.
Tests
The new
@safetestset \"UniformStep + Auto on AbstractRange (issue #7)\"covers:Base.searchsorted{first,last}(r, x[, order])acrossUnitRange,StepRange,StepRangeLen,LinRange, forward andreverse-direction ranges.
Reverseordering on every range flavor.xoutside the range,xat exact grid points.BinaryBracket.AutoonAbstractRangeproduces the right answer with minimalallocation (kwarg trampoline only — < 64 bytes per call).
AbstractRangewith sorted and unsorted queries.85042 tests pass (+416 from the new UniformStep tests).
Docs
docs/src/strategies.mdaddsUniformStep.@docsentry for the type.NEWS.mdentry under "Unreleased".🤖 Note for reviewer: this PR is in draft. Please ignore until reviewed
by @ChrisRackauckas.