The 2.x sorted-search API was a single generic
Base.searchsortedlast(::SearchStrategy, v, x[, hint]) /
Base.searchsortedfirst(::SearchStrategy, ...) extended once per
concrete strategy struct. That design made dispatch type-stable on the
chosen strategy but produced Union returns whenever the strategy
itself depended on runtime data — in particular Auto's decision tree,
where the strategy struct returned by _auto_pick(v, hint) had a
runtime-dependent type, broke @inferred, and gave Vector{Auto}-style
containers a Union element type.
The 3.0 redesign replaces the multimethod dispatch on SearchStrategy
singletons with a single FFF-owned dispatcher tagged by an enum:
searchsorted_last(KIND_BRACKET_GALLOP, v, x, hint)
searchsorted_first(KIND_INTERPOLATION_SEARCH, v, x)The runtime if/elseif over StrategyKind values is well-predicted in
hot loops, the kernel bodies inline, and the return path stays concrete
(Int) regardless of which kind is picked at runtime. A benchmark sweep
across 20 representative cells measured ~0 ns of overhead vs. the v2
multimethod path.
v3 no longer extends Base.searchsortedlast / Base.searchsortedfirst
with strategy methods. The FFF-owned searchsorted_last / searchsorted_first
dispatchers are the only search entry points; they accept a
StrategyKind tag, a strategy struct (which forwards through
[strategy_kind] and constant-folds for literal strategies), or a
stateful strategy (Auto, GuesserHint):
| v2 (removed) | v3 |
|---|---|
searchsortedlast(BracketGallop(), v, x, hint) |
searchsorted_last(KIND_BRACKET_GALLOP, v, x, hint) or searchsorted_last(BracketGallop(), v, x, hint) |
searchsortedfirst(InterpolationSearch(), v, x) |
searchsorted_first(KIND_INTERPOLATION_SEARCH, v, x) or searchsorted_first(InterpolationSearch(), v, x) |
searchsortedlast(UniformStep(), r, x) |
searchsorted_last(KIND_UNIFORM_STEP, r, x) or searchsorted_last(UniformStep(), r, x) |
searchsortedlast(Auto(v), v, x, hint) |
searchsorted_last(Auto(v), v, x, hint) |
searchsortedfirst(GuesserHint(g), v, x) |
searchsorted_first(GuesserHint(g), v, x) |
(The same rename applies to every other singleton strategy.) The batched
in-place API (searchsortedlast! / searchsortedfirst! /
searchsortedrange) is FFF-owned and unchanged.
Stateful strategies (Auto, GuesserHint) stay on the multimethod path
because they carry per-instance data.
In v2, Auto()'s per-query Base.searchsortedlast(::Auto, v, x, hint)
ran the picking logic on every call (consulting length(v), hint
validity, and props.is_uniform). In v3, Auto carries a resolved
StrategyKind and per-query dispatch is a one-line forward to
searchsorted_last(s.kind, v, x, hint):
Auto()defaults toKIND_BINARY_BRACKET(safe; matchesBase.searchsortedlastexactly).Auto(v)resolves the kind fromlength(v)+SearchProperties(v). Pre-pay the probe cost once, get the v2 fast-path on every per-query call afterwards.Auto(v, props)is the same with a pre-computedpropscache.
The batched Auto dispatch (searchsortedlast!(out, v, queries; strategy = Auto())) still re-resolves the kind from (v, queries)
because the gap heuristic needs the queries; that decision tree is
type-stable in v3 (returns a StrategyKind, dispatched via the enum
switch into a kind-parameterized loop).
The v2 behaviour of Auto() re-picking on every per-query call is
preserved for batched calls and for callers that explicitly construct
Auto(v) per query. For callers that previously relied on Auto() (no
v) picking LinearScan on short vectors or BracketGallop on long
vectors per-query, update to Auto(v) where v is known.
SearchProperties is now parametric on the data ratio type
T = typeof(oneunit(eltype(v)) / oneunit(eltype(v))) (Float64 for
Vector{Int} or Vector{Float64}, Float32 for Vector{Float32}, etc.).
The struct carries two new fields:
first_val::T—v[1](orfirst(r)for anAbstractRange).inv_step::T— the precomputed reciprocal1 / step. For anAbstractRange,1 / step(r). For an exactly-uniformAbstractVector,(length(v) - 1) / (v[end] - v[1]).
These fields are populated when is_uniform = true and zero otherwise.
For AbstractVector data, is_uniform is detected by the cheap 11-point
sampled pre-filter and then confirmed by an exact O(n) scan over every
element. The exact pass matters: data that is uniform at the sampled
points but jittered between them would otherwise be flagged uniform, and
the closed-form lookup would land in the wrong cell. The kernels also
carry a correction walk, so even a caller-forced is_uniform = true on
non-uniform data degrades to a slower search rather than a wrong answer.
The populated fields feed the new props-aware UniformStep kernel
invoked by Auto(v) when the resolved kind is KIND_UNIFORM_STEP:
# v3 closed-form O(1) lookup with no per-query division:
a = Auto(0.0:0.5:100.0) # kind = KIND_UNIFORM_STEP, inv_step = 2.0
searchsorted_last(a, r, 3.7) # → floor((3.7 - 0.0) * 2.0) + 1 = 8This subsumes the never-merged DirectStep strategy (PR #74) — its
precomputed-reciprocal closed-form is now folded into UniformStep via
the SearchProperties{T} payload.
The raw UniformStep() singleton kept its old behaviour: when called via
searchsortedlast(UniformStep(), r, x) (no props) it still uses
fld(diff, step) per query. Auto routes through the props-aware kernel
because Auto carries the precomputed SearchProperties{T}.
Auto now carries props::SearchProperties{T} and is itself parametric
on T. Auto(v) returns an Auto{T} where T is the ratio type of
eltype(v). Two Autos constructed from data with the same ratio type
(e.g. Vector{Int} and Vector{Float64} both → Auto{Float64}) share
a single concrete type, so Vector{Auto{Float64}} is concrete.
strategy_kind(s::SearchStrategy) maps a singleton strategy struct to
its StrategyKind tag, and returns the stored kind for Auto.
GuesserHint (genuinely stateful, no singleton tag) throws
ArgumentError.
findequal(KIND_BRACKET_GALLOP, v, x)
findequal(KIND_BRACKET_GALLOP, v, x, hint)In addition to the findequal(strategy_struct, v, x[, hint]) form,
which forwards through the same struct → kind mapping.
The v2 src/dispatch.jl file (Base.searchsortedlast extensions per
strategy) is gone. In its place:
src/kinds.jl—StrategyKindenum andsearchsorted_last/searchsorted_firstenum dispatchers.src/kernels.jl— per-strategy kernel functions (_kernel_last_bracket_gallop, etc.), lifted out of the v2 method bodies.src/strategy_kind.jl— the struct → kind mapping plus the struct-valuedsearchsorted_last(::S, ...)/searchsorted_first(::S, ...)entry points that forward through it.
This is a major rewrite of the sorted-search API. The 1.x surface — a
collection of single-purpose, hint-flavoured function names — has been
replaced by a single strategy-dispatched API where the algorithm is chosen
at the call site (or by Auto) rather than baked into the function name.
The following 1.x functions are gone in 2.0. Each one has a single canonical 2.x replacement:
| 1.x | 2.x |
|---|---|
searchsortedfirstcorrelated(v, x, guess::Integer) |
searchsortedfirst(BracketGallop(), v, x, guess) |
searchsortedlastcorrelated(v, x, guess::Integer) |
searchsortedlast(BracketGallop(), v, x, guess) |
searchsortedfirstcorrelated(v, x, g::Guesser) |
searchsortedfirst(GuesserHint(g), v, x) |
searchsortedlastcorrelated(v, x, g::Guesser) |
searchsortedlast(GuesserHint(g), v, x) |
searchsortedfirstvec(v, qs) |
searchsortedfirst!(buf, v, qs) (caller-owned buf) |
searchsortedlastvec(v, qs) |
searchsortedlast!(buf, v, qs) (caller-owned buf) |
The *vec migration shifts buffer ownership to the caller. The in-place
form lets callers reuse buffers across calls, which the allocating form
couldn't. For one-shot use the caller-side allocation is a single
Vector{Int}(undef, length(qs)) per call site.
These helpers backed 1.x public names. In 2.0 they remain in the module as implementation details of the strategy dispatch, but are no longer documented or part of the public API:
searchsortedfirstexp— now backs theExpFromLeftstrategy. Usesearchsortedfirst(ExpFromLeft(), v, x, lo)instead.bracketstrictlymontonic— now backs theBracketGallopstrategy. Callers wanting a bracket-then-binary-search should usesearchsortedlast(BracketGallop(), v, x, hint)/searchsortedfirst(BracketGallop(), v, x, hint).
A single pair of generic functions covers every sorted-search algorithm in the package:
searchsortedfirst(strategy, v, x[, hint]; order = Base.Order.Forward)
searchsortedlast(strategy, v, x[, hint]; order = Base.Order.Forward)strategy is a concrete subtype of SearchStrategy. The shipped
strategies are:
LinearScan— walks ±1 from the hint. Cheapest when the hint is close to the answer; O(n) worst case.SIMDLinearScan—LinearScanwith the forward walk lowered to 8-wide SIMD chunks via custom LLVM IR. Specialized forDenseVector{Int64}andDenseVector{Float64}; falls back to scalarLinearScanfor any other element type. Opt-in only —Autodoes not pick it. Seestrategies.mdfor the NaN / element-type caveats.BracketGallop— bidirectional exponential bracket around the hint, then bounded binary search. Workhorse hinted strategy. O(1) when the hint is close, never worse than ~2 log₂ n.ExpFromLeft— exponential search forward from a left-bound hint. Five linear probes, then doubling, then bounded binary search. DefaultAutochoice in the sparse-batched path.InterpolationSearch— linear-extrapolation guess refined with binary search. O(1) per query on uniformly-spaced numeric data, O(log n) otherwise.BinaryBracket— plainBase.searchsortedlast/Base.searchsortedfirst. Used as the no-hint fallback by every other strategy.GuesserHint(g::Guesser)—BracketGallopdriven by aGuesser's integer guess, with the result cached back into theGuesser.Auto— heuristic dispatcher; see "New:Autoheuristic" below.
Strategies are zero-field singletons (except GuesserHint, which wraps a
Guesser, and Auto, which optionally carries a SearchProperties
cache). The dispatch is type-stable; pinning a strategy at a call site
costs nothing at runtime.
searchsortedfirst!(idx_out, v, queries; strategy = Auto(), order = ...)
searchsortedlast!(idx_out, v, queries; strategy = Auto(), order = ...)Writes one index per element of queries into idx_out (which must be
the same length). If queries is sorted under order, each query's hint
is the previous result, so the total cost for sorted batches is
O(length(v) + length(queries)) under the typical Auto choice. If
queries is not sorted, the call falls back to per-element
Base.searchsortedlast / Base.searchsortedfirst with no hint.
These methods are the replacement for the removed searchsortedfirstvec /
searchsortedlastvec. The caller owns the output buffer and is free to
reuse it across calls.
Auto picks a strategy based on the calling context:
Per-query (searchsortedlast(Auto(), v, x[, hint])):
- No hint, or hint out of range →
BinaryBracket. - Hint in range,
length(v) ≤ 16→LinearScan. - Hint in range,
length(v) > 16→BracketGallop.
Batched sorted (searchsortedlast!(out, v, queries; strategy = Auto()))
chooses by the expected average gap in v's index space between
consecutive query results. For numeric data the gap is estimated from the
span ratio (queries[end] - queries[1]) / (v[end] - v[1]), so dense-burst
queries clustered inside one segment of v are correctly recognized as
gap ≈ 0:
gap ≤ 4→LinearScan.gap ≥ 8,length(v) ≥ 1024,length(queries) ≥ 2, not skewed, and a sampled-linearity probe accepts →InterpolationSearch.- otherwise →
ExpFromLeft.
The sampled-linearity probe reads 11 elements (~25 ns) and accepts when
every interior point sits within 0.1% of the straight line through v[1]
and v[end]. The 0.1% tolerance is tight by design: at large n the
order-statistic variance of random-sorted data is small enough that a 5%
threshold would falsely pass on irregular data.
Skew detection on the query distribution adds an additional gate: if the
median query is more than 20% off the midpoint of the query span (and
m ≥ 10 so the median is meaningful), Auto picks ExpFromLeft even
on linear v, because consecutive queries land in the same region and
the previous-result hint is worth more than the linear-extrapolation
guess. Skew detection is gated on m ≥ 10 to avoid the median sampling
variance dominating for small batches.
The crossover constants (_AUTO_LINEAR_THRESHOLD = 16,
_AUTO_BATCH_LINEAR_GAP = 4, _AUTO_INTERP_MIN_GAP = 8,
_AUTO_INTERP_MIN_N = 1024, _AUTO_INTERP_MIN_M = 2,
_AUTO_LINEAR_REL_TOLERANCE = 1e-3) were tuned empirically by a regime
grid covering uniform / jittered / log-spaced / two-scale / random v
patterns crossed with dense / sparse / clustered / sorted-random query
patterns at vector lengths from 64 to 65536 and batch sizes from 1 to
4096. Across that grid Auto is within 1.2× of the per-cell optimum in
90% of cells.
For callers issuing many short batches against the same sorted vector
(interpolation-segment lookups being the obvious case), Auto's per-call
linearity probe is redundant. The new SearchProperties struct caches
the probe result and Auto(props) consumes it instead of re-probing:
v = collect(0.0:0.001:100.0)
props = SearchProperties(v) # run probes once: ~25 ns + (Float-only) O(n) NaN scan
strat = Auto(props) # Auto holding the cached facts
queries = sort!(rand(8) .* 100.0)
out = Vector{Int}(undef, length(queries))
searchsortedlast!(out, v, queries; strategy = strat)SearchProperties is isbits — it travels in registers and copies are
free. Auto(props) is itself zero-allocation; the resulting Auto is a
single concrete struct, not a parametric type.
Currently consumed: props.is_linear (replaces _sampled_looks_linear
in the batched dispatch). The has_props and has_nan fields are
populated by SearchProperties(v) for forward compatibility; the latter
will unlock SIMDLinearScan participation in Auto once the eligibility
gate is wired in.
The cache is not invalidated automatically — the caller must reconstruct
SearchProperties(v) if v mutates. A stale cache is correctness-
preserving (the chosen InterpolationSearch falls through to
BracketGallop from a bad guess — slow but still O(log n)), so the
worst case is a performance regression, not a wrong answer.
A SIMD variant of LinearScan that lowers the forward walk past the hint
to 8-wide SIMD chunks via custom LLVM IR. The same scaffolding that backs
_findfirstequal (load 8 lanes, vector compare, bitmask, cttz on the
mask) is reused for the four predicates needed by positional search:
_simd_first_gt/_simd_first_geforInt64(usingicmp sgt/icmp sge)._simd_first_gt/_simd_first_geforFloat64(usingfcmp ogt/fcmp oge).
The IR is generated from a shared template _simd_scan_ir(t, pred)
parameterised on LLVM element type and compare predicate.
Caveats (documented in detail in strategies.md):
- Element types:
DenseVector{Int64}andDenseVector{Float64}only. Other element types (includingInt32,UInt64,Float32,Date,String) hit the scalarLinearScanfallback path. The dispatch is static, so the fallback costs nothing per call. - NaN: ordered float compares (
fcmp o*) return false for NaN operands, so a NaN invis silently skipped by the SIMD scan. Sorted-Float64 with NaN isn't well-defined under any total order anyway, so this is consistent withBase.searchsortedlaston such vectors. - Forward order only: non-
Forwardorderings fall back to scalarLinearScan. - No hint: falls back to
BinaryBracket.
Auto does not pick SIMDLinearScan. It is opt-in: the regime where
it strictly beats LinearScan (long forward walks on Int64/Float64
DenseVectors) overlaps with the regime where Auto already prefers
BracketGallop or ExpFromLeft. Pin it explicitly when you have a
workload that wants a long forward scan and you know the element type.
The single index.md from 1.x has been split into five topical pages:
- Home (
index.md): overview and quick example. - Interface and extension rules (
interface.md): the public API surface, the contract aSearchStrategysubtype must satisfy, and how to add a new one with a correctness-check pattern. Notes thatAuto's decision tree is not externally extensible — new strategies do not auto-register withAuto. - Search strategies (
strategies.md): catalog of every shipped strategy with a chooser table (best case / worst case / hint usage), per-strategy notes, theSIMDLinearScancaveats, and the "Equality search" appendix linking tofindfirstequal/findfirstsortedequal(which are a deliberately-separate API because their return type differs from positional search). - Guessers (
guessers.md): theGuessertype, its linear-extrapolation vs. cached-previous-result behaviour, theGuesserHintstrategy adapter, and explicit guidance on when not to use aGuesser. - Auto: heuristics and benchmarks (
auto.md): fullAutodecision tree for both per-query and batched callers, every crossover constant with justification, theSearchPropertiescache integration, and a self-contained benchmark script that reproduces the regime-grid comparison.
The LLVM IR pattern used by _findfirstequal (load 8 lanes, vector
compare, cttz on the bitmask) is now generated by a shared template
_simd_scan_ir(t, pred). FFE_IR (equality scan, used by
findfirstequal and findfirstsortedequal) and the four
_SIMD_*_IRs (positional compares for SIMDLinearScan) all flow from
that template. Adding a new predicate is a one-line change.
findequal(strategy, v, x[, hint]) builds an equality variant on top of
the strategy dispatch. The return type is Int (not Union{Int, Nothing});
"not found" is signalled by the sentinel firstindex(v) - 1 (= 0 on
1-based vectors), matching the convention Base.searchsortedlast already
uses for "x precedes all of v".
- Most strategies are handled generically:
findequal(strategy, v, x[, hint])runssearchsortedfirst(strategy, v, x[, hint])and checks whether the candidate index actually equalsx. This meansfindequal(BracketGallop(), v, x, hint),findequal(SIMDLinearScan(), v, x, hint),findequal(GuesserHint(g), v, x),findequal(Auto(), v, x), andfindequal(BinaryBracket(), v, x)all work without per-strategy glue. BisectThenSIMD <: SearchStrategyis a new strategy that, forDenseVector{Int64}, dispatchesfindequalstraight into the bisect-then-SIMD-equality-scan algorithm that backsfindfirstsortedequal. For other element types, falls back toBinaryBracket + post-check. In positional dispatch (searchsortedfirst/searchsortedlast) it delegates toBinaryBracket— the bisect-then-equality-scan algorithm can't answer the positional "where wouldxinsert?" question.
Four pre-existing functions returned the wrong index when v contained
duplicates of the queried value and the hint or bisection midpoint
landed inside a run of duplicates. All four are fixed in 2.0:
searchsortedfirst(BracketGallop(), v, x, hint)previously galloped rightward whenv[hint] == x, returning the rightmost duplicate instead of the first. Fixed by adding the companionbracketstrictlymontonic_firstthat gallops leftward whenv[hint] >= x.searchsortedfirst(InterpolationSearch(), v, x)chains intoBracketGallop, so the same bug propagated and is fixed by the above.searchsortedfirst(ExpFromLeft(), v, x, hint)previously exponential-searched fromhintwhenv[hint] == x, missing earlier duplicates. Fixed by falling back to a full search wheneverv[hint] >= x.findfirstsortedequal(var, vars)bisected with the predicatevars[mid] <= var, which walked the offset past the first duplicate whenvars[mid] == var. Fixed by tightening the predicate to<and updating the window-shrink rule to include the midpoint when the comparison is false. The fast-path LLVM IR branch is replaced by plainifelse(Julia compiles it to the same branchlessselectmodulo the!unpredictablemetadata, which had minimal observable effect).
The fix is exercised by the new findequal strategy-parity tests on
randomized vectors with frequent duplicates (Int64 in [-50, 50] over
vectors up to length 256, 2000 trials per strategy across all shipped
strategies).
Both names continue to exist in 2.0, returning Union{Int, Nothing} as
before. Docstrings refreshed to point at the new
[findequal](@ref FindFirstFunctions.findequal) wrapper as the
strategy-framework-compatible alternative. Documentation moved out of
strategies.md into a dedicated equality.md page since these functions
do not match the strategy-dispatch contract (their return type and
question are different).
2.0 exports the public API surface (previously the package exported
nothing, requiring FindFirstFunctions.LinearScan() qualification):
SearchStrategy, every concrete strategy (LinearScan,SIMDLinearScan,BracketGallop,ExpFromLeft,InterpolationSearch,BinaryBracket,BisectThenSIMD,GuesserHint,Auto), and theSearchPropertiescache.Guesserandlooks_linear.- The batched FFF-defined names
searchsortedfirst!andsearchsortedlast!(the non-bangsearchsortedfirst/searchsortedlastareBaseextensions, available viaBase). - The equality routines
findequal,findfirstequal,findfirstsortedequal.
using FindFirstFunctions is now sufficient to access the full public
API. Downstream code that previously qualified every call (most of the
SciML ecosystem) continues to work — the qualified names still resolve.
Auto's batched decision tree has been retuned based on a 1080-cell
benchmark sweep covering 5 v patterns × 4 query patterns × 5 n sizes ×
6 m sizes × 2 element types. The previous tree fell out of the bench
sweep with median 1.18× / p95 2.09× / max 2.78× slack against the per-cell
optimum; the retuned tree comes in at median 1.04× / p95 1.38× /
max 2.18×.
New branches and constants:
SIMDLinearScanis now dispatched byAutoin the medium-gap regime (gap ∈ (4, _auto_simd_gap_max(v)]) whenvisDenseVector{Int64}orDenseVector{Float64}._auto_simd_gap_maxis 64 for both eltypes. For Float64 the dispatch consultsSearchProperties.has_nanif available; otherwise no-NaN is assumed, consistent with howBase.searchsortedlastalready trusts the input is sorted.BracketGallopis preferred overExpFromLeftatgap ≥ 16(new constant_AUTO_GALLOP_GAP_MIN). The five up-front linear probes ofExpFromLeftare guaranteed to miss once the answer is more than five elements past the previous-result hint, so the doubling-from-hintwalk ofBracketGallopis strictly faster at large gaps.- Tiered linearity probe for
InterpolationSearch. The strict_AUTO_LINEAR_REL_TOLERANCE = 1e-3still gates the_AUTO_INTERP_MIN_GAP ≤ gap < _AUTO_INTERP_LOOSE_GAP(8 to 256) range — only truly uniform data passes. Forgap ≥ _AUTO_INTERP_LOOSE_GAP(256), the loose_AUTO_LINEAR_LOOSE_TOLERANCE = 5e-2applies, which accepts approximately linear data (sorted random, jittered) where the O(√n)/n order-statistic deviation is well below 5 %.InterpolationSearchstill loses on log-spaced and two-scale at any gap, and the strict tier catches those.
Bug fix: _estimate_avg_gap no longer falls back to n ÷ m when the
skew flag is set. The fallback caused SIMDLinearScan to be picked for
tightly-clustered queries (span_q ≈ 0) where LinearScan's scalar walk
is 5× faster. The skew flag now serves its intended purpose as a binary
InterpolationSearch-unsuitability signal, while the actual gap value is
always the span-based estimate.
Reproducibility: the full sweep is checked in at bench/auto_sweep.jl
with an analysis helper at bench/analyze.jl. See auto.md for the
decision tree, the per-regime winner distribution, and how to run the
sweep locally.
BitInterpolationSearch is a variant of InterpolationSearch that
reinterprets a positive DenseVector{Float64} as DenseVector{UInt64}
before computing the extrapolation guess. The IEEE bit pattern is
monotonically increasing with the float value (for positive Float64) and
approximately linear in array index when the underlying data is
log-spaced (geometric). On such data the bit-domain guess can be far
better than the float-domain guess that InterpolationSearch would
compute.
A targeted bench sweep at bench/bitinterp_sweep.jl covers 9 v patterns
× 4 q patterns × 6 n sizes (up to 2²⁰) × 7 m sizes, with v patterns
specifically chosen to probe BitInterp's regime: logspaced (1 to 10⁶),
logspaced_wide (10⁻³ to 10¹⁵), geometric_dense (geometric spanning
10⁶), geometric_sparse (geometric spanning 10¹²), power2, sqrt,
two_decade, jittered_log, and uniform (as the negative control).
Result over 1404 cells: BitInterp wins outright in 59 cells (4.2%) and
sits within 10% of the per-cell best in 75 cells (5.3%). The wins
concentrate in logspaced_wide / logspaced / geometric_* /
jittered_log at small m (= 4) and large n (≥ 16384). Margins range
from 1.0× (tie) to 1.43× (logspaced_wide log_grid n=2²⁰ m=4). On
non-log-spaced data (uniform, power2, sqrt, two_decade)
BitInterp loses — the bit-pattern guess is worse than the float-domain
guess when the data isn't geometric.
Auto does not pick BitInterp. The wins are real but narrow
(small batches, very large n, true log-spacing), and adding the dispatch
overhead to Auto's hot path would penalize the much larger set of
non-log-spaced workloads. The strategy is exported as BitInterpolationSearch
for callers who know their data is log-spaced and want to pin it.
Constraints:
DenseVector{Float64}only; non-Float64 dense eltypes fall back to plainInterpolationSearch.- Requires
v[1] > 0,x > 0, and both finite. Subnormal / non-finite Float64 bit patterns are not monotonic with float value under raw reinterpret, so the strategy falls back toBinaryBracketin those cases. - Forward ordering only.
- Internal helper
bracketstrictlymontonicrenamed tobracketstrictlymonotonic(and the companion_firstvariant). Internal-only — no downstream impact. - The
FFE_IRSIMD-equality IR literal is now generated by_simd_scan_ir("i64", "eq")instead of duplicating ~60 lines of inline LLVM IR. The same template produces the four>/>=variants forSIMDLinearScan, so all five SIMD primitives share a single source of truth. SearchProperties(v; linear_tolerance = 1e-3)exposes the sampled-linearity probe's tolerance as a kwarg, matchingGuesser'slooks_linear_threshold. Loosen (e.g.1e-2) to widen the regime whereAuto(props)picksInterpolationSearch; tighten (e.g.1e-4) to be more conservative. Default unchanged at1e-3.
findequal's docstring now explicitly documents the sentinel value
firstindex(v) - 1 and its behaviour on OffsetArrays.
- Julia compat: unchanged from 1.x —
julia = "1.10". - Downstream PRs: SciML packages using the removed names need
companion PRs. The first one
SciML/DataInterpolations.jl#529
is the migration template: drop the legacy imports, route the
Integer-hint path throughsearchsortedfirst(BracketGallop(), …)and theGuesserpath throughsearchsortedfirst(GuesserHint(g), …).
47100 tests pass across the strategy dispatch, the batched API, the
Auto heuristic on a regime sweep, SIMDLinearScan randomized fuzz
(10000 Int64 + 10000 Float64 cases against Base), edge cases (empty,
single-element, duplicates, out-of-range hints, x outside the vector
range), fallback paths (Int32, Float32, String, no-hint, reverse
order), and SearchProperties cache correctness (output equivalence
against the un-cached path, isbits guarantee, behaviour under lying
cache).