Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ callers who already know their access pattern and want to pin a strategy.
| [`InterpolationSearch`](@ref FindFirstFunctions.InterpolationSearch) | `v` is uniformly spaced and numeric | O(1) | O(log n) | no |
| [`BitInterpolationSearch`](@ref FindFirstFunctions.BitInterpolationSearch) | `DenseVector{Float64}` and log-spaced (geometric) — opt-in, `Auto` does not pick | O(1) | O(log n) | no |
| [`BinaryBracket`](@ref FindFirstFunctions.BinaryBracket) | no hint available, or fallback | O(log n) | O(log n) | no |
| [`UniformStep`](@ref FindFirstFunctions.UniformStep) | `v isa AbstractRange` (or known-uniformly-spaced) | O(1) | O(1) | no |
| [`GuesserHint`](@ref FindFirstFunctions.GuesserHint) | repeated correlated lookups against the same `v` | O(1) | ~2 log₂ n | self-provided |
| [`Auto`](@ref FindFirstFunctions.Auto) | unknown access pattern | varies | varies | yes if supplied |

Expand All @@ -42,6 +43,7 @@ FindFirstFunctions.InterpolationSearch
FindFirstFunctions.BitInterpolationSearch
FindFirstFunctions.BinaryBracket
FindFirstFunctions.BisectThenSIMD
FindFirstFunctions.UniformStep
FindFirstFunctions.Auto
FindFirstFunctions.SearchProperties
```
Expand Down
2 changes: 1 addition & 1 deletion src/FindFirstFunctions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export
SearchStrategy,
LinearScan, SIMDLinearScan, BracketGallop, ExpFromLeft,
InterpolationSearch, BitInterpolationSearch,
BinaryBracket, BisectThenSIMD,
BinaryBracket, UniformStep, BisectThenSIMD,
GuesserHint, Auto,
SearchProperties,
Guesser, looks_linear,
Expand Down
65 changes: 46 additions & 19 deletions src/auto.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ end
end
@inline _auto_simd_eligible(::AbstractVector, ::SearchProperties) = false

# Uniformity check used by Auto to short-circuit to `UniformStep`. Two
# routes: the static type test catches `AbstractRange` (always uniform by
# definition); the props check catches `Vector` callers who supplied
# `SearchProperties(v; is_uniform = true)`.
@inline _auto_is_uniform(::AbstractRange, ::SearchProperties) = true
@inline _auto_is_uniform(::AbstractVector, p::SearchProperties) =
p.has_props && p.is_uniform

# InterpolationSearch eligibility: two-tier linearity check. For
# `_AUTO_INTERP_MIN_GAP ≤ gap < _AUTO_INTERP_LOOSE_GAP` we require strict
# linearity (`_AUTO_LINEAR_REL_TOLERANCE`, default 0.1%) — InterpolationSearch
Expand All @@ -137,32 +145,51 @@ end
return props.has_props ? props.is_linear : _sampled_looks_linear(v)
end

# Per-query Auto dispatch.
# Per-query Auto dispatch. Checks `is_uniform` first so `AbstractRange`
# inputs (always uniform) and `Vector`s with `SearchProperties(v;
# is_uniform = true)` short-circuit to `UniformStep`'s closed-form path
# without paying for `_auto_pick`'s hint validity check.
function Base.searchsortedlast(
::Auto, v::AbstractVector, x, hint::Integer;
s::Auto, v::AbstractVector, x, hint::Integer;
order::Base.Order.Ordering = Base.Order.Forward,
)
s = _auto_pick(v, hint)
return s isa BinaryBracket ?
searchsortedlast(s, v, x; order = order) :
searchsortedlast(s, v, x, hint; order = order)
if _auto_is_uniform(v, s.props)
return searchsortedlast(UniformStep(), v, x; order = order)
end
chosen = _auto_pick(v, hint)
return chosen isa BinaryBracket ?
searchsortedlast(chosen, v, x; order = order) :
searchsortedlast(chosen, v, x, hint; order = order)
end

function Base.searchsortedfirst(
::Auto, v::AbstractVector, x, hint::Integer;
s::Auto, v::AbstractVector, x, hint::Integer;
order::Base.Order.Ordering = Base.Order.Forward,
)
s = _auto_pick(v, hint)
return s isa BinaryBracket ?
searchsortedfirst(s, v, x; order = order) :
searchsortedfirst(s, v, x, hint; order = order)
if _auto_is_uniform(v, s.props)
return searchsortedfirst(UniformStep(), v, x; order = order)
end
chosen = _auto_pick(v, hint)
return chosen isa BinaryBracket ?
searchsortedfirst(chosen, v, x; order = order) :
searchsortedfirst(chosen, v, x, hint; order = order)
end

Base.searchsortedlast(
::Auto, v::AbstractVector, x;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedlast(BinaryBracket(), v, x; order = order)
Base.searchsortedfirst(
::Auto, v::AbstractVector, x;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedfirst(BinaryBracket(), v, x; order = order)
function Base.searchsortedlast(
s::Auto, v::AbstractVector, x;
order::Base.Order.Ordering = Base.Order.Forward,
)
if _auto_is_uniform(v, s.props)
return searchsortedlast(UniformStep(), v, x; order = order)
end
return searchsortedlast(BinaryBracket(), v, x; order = order)
end
function Base.searchsortedfirst(
s::Auto, v::AbstractVector, x;
order::Base.Order.Ordering = Base.Order.Forward,
)
if _auto_is_uniform(v, s.props)
return searchsortedfirst(UniformStep(), v, x; order = order)
end
return searchsortedfirst(BinaryBracket(), v, x; order = order)
end
17 changes: 17 additions & 0 deletions src/batched.jl
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ function _searchsortedlast_batched!(
s::Auto, order::Base.Order.Ordering,
queries_sorted::Union{Nothing, Bool},
)
# Uniform-spaced vectors (always true for `AbstractRange`, optionally
# for `Vector`s carrying `SearchProperties(v; is_uniform = true)`) go
# straight to the closed-form `UniformStep` path — no gap estimation,
# no linearity probe, no `issorted` check (uniformly-spaced sorted
# data has the same answer regardless of query ordering).
if _auto_is_uniform(v, s.props)
@inbounds for k in eachindex(queries)
idx_out[k] = searchsortedlast(UniformStep(), v, queries[k]; order = order)
end
return idx_out
end
m = length(queries)
m == 0 && return idx_out
# m == 1: skip the issorted + span heuristic — no batched hint is
Expand Down Expand Up @@ -237,6 +248,12 @@ function _searchsortedfirst_batched!(
s::Auto, order::Base.Order.Ordering,
queries_sorted::Union{Nothing, Bool},
)
if _auto_is_uniform(v, s.props)
@inbounds for k in eachindex(queries)
idx_out[k] = searchsortedfirst(UniformStep(), v, queries[k]; order = order)
end
return idx_out
end
m = length(queries)
m == 0 && return idx_out
if m == 1
Expand Down
151 changes: 151 additions & 0 deletions src/dispatch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,157 @@ Base.searchsortedfirst(
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedfirst(InterpolationSearch(), v, x; order = order)

# ===========================================================================
# Strategy: UniformStep — O(1) direct-arithmetic lookup for AbstractRange.
#
# The answer is computed by solving the equation `r[i] = first(r) + (i -
# firstindex(r)) * step(r)`. For `searchsortedlast`, the answer is the
# largest `i` with `r[i] <= x` (under `Forward`) or `r[i] >= x` (under
# `Reverse`); for `searchsortedfirst`, the smallest `i` with the
# meets-or-passes condition. Either way it's a single floor/ceil + clamp.
#
# Same formula works for both `Forward` and `Reverse` ordering because
# `step(r)` carries the direction in its sign — a decreasing range has
# negative step, which flips the inequality when dividing.
#
# For non-Range vectors `step(v)` isn't defined, so the strategy falls
# back to `BinaryBracket`. For custom orderings (`By`, `Lt`, …) — also
# falls back, since the closed-form arithmetic assumes the underlying `<`
# ordering of `Forward` / `Reverse`.
# ===========================================================================

@inline _uniformstep_supported_order(::Base.Order.ForwardOrdering) = true
@inline _uniformstep_supported_order(::Base.Order.ReverseOrdering) = true
@inline _uniformstep_supported_order(::Base.Order.Ordering) = false

# The closed-form formula: `r[i] = first(r) + (i - firstindex(r)) * step(r)`
# solves for `i` as `firstindex(r) + fld((x - first(r)), step(r))` for
# `searchsortedlast`. `fld`/`cld` are defined for both integer and
# floating-point operands, keeping Int operands in exact Int math (matching
# Base's UnitRange{Int} fast path) while letting Float64 inputs use Float64
# math directly.
#
# Float-eltype ranges (`StepRangeLen`, `LinRange`) suffer from the standard
# float-roundoff issue: e.g. `5.0 / 0.1 ≈ 49.9999…` so the naive `fld`
# returns 49 instead of 50. Base's range-aware `searchsortedlast` uses
# `TwicePrecision` internally to avoid this. We don't want to pay that
# arithmetic cost; instead we use the naive `fld` as a *rough* estimate
# and add a one-step correction by checking `r[i+1]` (or `r[i]`) against
# `x`. The correction is at most one increment in either direction because
# `fld` rounding error is bounded by one ulp of the divisor — much less
# than one step of the range.
@inline function _uniformstep_searchsortedlast(
r::AbstractRange, x, order::Base.Order.Ordering,
)
isempty(r) && return firstindex(r) - 1
s = step(r)
iszero(s) && return lastindex(r)
diff = x - first(r)
# Reject non-finite query positions upfront — NaN / Inf would propagate
# through fld unpredictably.
if diff isa AbstractFloat && !isfinite(diff)
return isnan(diff) ? (firstindex(r) - 1) :
(diff > 0) ⊻ (s < 0) ? lastindex(r) : firstindex(r) - 1
end
nm1 = length(r) - 1
f = fld(diff, s)
# Clamp before the correction step so we don't index off the array.
i = if f < 0
firstindex(r) - 1
elseif f >= nm1
lastindex(r)
else
firstindex(r) + Int(f)
end
# Roundoff correction: float division can be one ulp off, so the
# computed index may be off by one. Compare against `r[i+1]` /
# `r[i]` using the order-aware predicate `!lt(order, x, ·)` ("v[·] is
# at or below the threshold under this ordering"). At most one
# increment in either direction.
@inbounds if i < lastindex(r) && !Base.Order.lt(order, x, r[i + 1])
return i + 1
elseif i >= firstindex(r) && i <= lastindex(r) && Base.Order.lt(order, x, r[i])
return i - 1
end
return i
end

@inline function _uniformstep_searchsortedfirst(
r::AbstractRange, x, order::Base.Order.Ordering,
)
isempty(r) && return firstindex(r)
s = step(r)
iszero(s) && return firstindex(r)
diff = x - first(r)
if diff isa AbstractFloat && !isfinite(diff)
return isnan(diff) ? (lastindex(r) + 1) :
(diff > 0) ⊻ (s < 0) ? lastindex(r) + 1 : firstindex(r)
end
nm1 = length(r) - 1
f = cld(diff, s)
i = if f <= 0
firstindex(r)
elseif f > nm1
lastindex(r) + 1
else
firstindex(r) + Int(f)
end
# Roundoff correction. searchsortedfirst's condition is "smallest i
# with `!lt(order, r[i], x)`" (`r[i] >= x` under Forward, `r[i] <= x`
# under Reverse). If `r[i-1]` already meets the condition the
# estimate is too high; if `r[i]` doesn't, the estimate is too low.
@inbounds if i > firstindex(r) && i <= lastindex(r) + 1 &&
!Base.Order.lt(order, r[i - 1], x)
return i - 1
end
@inbounds if i <= lastindex(r) && Base.Order.lt(order, r[i], x)
return i + 1
end
return i
end

Base.searchsortedlast(
s::UniformStep, v::AbstractRange, x;
order::Base.Order.Ordering = Base.Order.Forward,
) = _uniformstep_supported_order(order) ?
_uniformstep_searchsortedlast(v, x, order) :
searchsortedlast(BinaryBracket(), v, x; order = order)
Base.searchsortedfirst(
s::UniformStep, v::AbstractRange, x;
order::Base.Order.Ordering = Base.Order.Forward,
) = _uniformstep_supported_order(order) ?
_uniformstep_searchsortedfirst(v, x, order) :
searchsortedfirst(BinaryBracket(), v, x; order = order)

# Hinted forms — UniformStep ignores hints (the closed form doesn't need one).
Base.searchsortedlast(
s::UniformStep, v::AbstractRange, x, ::Integer;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedlast(s, v, x; order = order)
Base.searchsortedfirst(
s::UniformStep, v::AbstractRange, x, ::Integer;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedfirst(s, v, x; order = order)

# Non-Range eltype: fall back to BinaryBracket. UniformStep's closed-form
# math requires `step(v)` to be exact, which is only true for AbstractRange.
Base.searchsortedlast(
::UniformStep, v::AbstractVector, x;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedlast(BinaryBracket(), v, x; order = order)
Base.searchsortedfirst(
::UniformStep, v::AbstractVector, x;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedfirst(BinaryBracket(), v, x; order = order)
Base.searchsortedlast(
::UniformStep, v::AbstractVector, x, ::Integer;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedlast(BinaryBracket(), v, x; order = order)
Base.searchsortedfirst(
::UniformStep, v::AbstractVector, x, ::Integer;
order::Base.Order.Ordering = Base.Order.Forward,
) = searchsortedfirst(BinaryBracket(), v, x; order = order)

# ===========================================================================
# Strategy: BisectThenSIMD — equality-search; positional dispatch falls back
# to BinaryBracket. (The `findequal(BisectThenSIMD(), v, x)` shortcut lives
Expand Down
Loading
Loading