Search strategies
The strategies form the parameter space of the sorted-search API. The entry points are searchsorted_last / searchsorted_first, and a strategy can be passed to them two ways:
- As a
StrategyKindenum value (e.g.KIND_BRACKET_GALLOP). One enum value per singleton strategy; runtimeif/elseifdispatch into the matching kernel; ~0 ns of overhead in hot loops; the inferred return type is concrete regardless of which kind is picked at runtime. Use this when the strategy is chosen from runtime data. - As a singleton strategy struct (e.g.
BracketGallop()). The struct forwards throughstrategy_kind, which constant-folds for a literal strategy argument — so for a literal strategy the struct form compiles to exactly the same code as the enum form.
FindFirstFunctions does not extend Base.searchsortedlast / Base.searchsortedfirst. The v2 API that did is removed in v3; these FFF-owned functions are the only search entry points.
The stateful strategies — Auto and GuesserHint — carry per-instance data and so cannot be expressed as singleton enum tags. They dispatch via their own multimethods.
FindFirstFunctions.SearchStrategy — Type
SearchStrategyAbstract supertype for sorted-search strategies. Two flavours of concrete subtype:
- Singleton strategies (
LinearScan,SIMDLinearScan,BracketGallop,ExpFromLeft,InterpolationSearch,BitInterpolationSearch,BinaryBracket,UniformStep,BisectThenSIMD) are zero-field structs. Each one has a matchingStrategyKindenum value; pass either the struct or its enum tag tosearchsorted_last/searchsorted_first. The struct form forwards throughstrategy_kindand constant-folds for a literal strategy, so it compiles to the same code as the enum tag. - Stateful strategies (
Auto,GuesserHint) carry per-instance data. They dispatch via their ownsearchsorted_last/searchsorted_firstmultimethods.
Strategies can also be passed to the batched searchsortedlast! / searchsortedfirst! APIs.
FindFirstFunctions.StrategyKind — Type
StrategyKindEnum tag identifying a singleton search strategy. Use values of this enum as the first positional argument to searchsorted_last and searchsorted_first:
searchsorted_last(KIND_BRACKET_GALLOP, v, x, hint)Each tag corresponds to one of the singleton strategy types (e.g. KIND_BRACKET_GALLOP ↔ BracketGallop). Stateful strategies (GuesserHint) do not have an enum tag — they dispatch through their wrapper struct directly.
The enum is stored as UInt8 so passing it through to dispatcher functions costs the same as a Bool and does not enlarge any struct that carries it.
FindFirstFunctions.searchsorted_last — Function
searchsorted_last(kind::StrategyKind, v, x[, hint]; order = Base.Order.Forward)
searchsorted_last(s, v, x[, hint]; order = Base.Order.Forward)FFF-owned positional search for the largest index i with v[i] ≤ x under order (or v[i] ≥ x under Base.Order.Reverse). The polarity matches Base.searchsortedlast.
v must be sorted in ascending order under order. Like Base.searchsortedlast, the precondition is assumed, not checked — the result on unsorted v is undefined.
When the first argument is a StrategyKind value the call dispatches via a runtime if/elseif branch on the enum into the matching kernel. When the first argument is a stateful strategy wrapper (Auto, GuesserHint) the call dispatches via multimethod into that wrapper's own searchsorted_last method.
This is the only search entry point: as of v3, FindFirstFunctions no longer extends Base.searchsortedlast / Base.searchsortedfirst with strategy methods.
FindFirstFunctions.searchsorted_first — Function
searchsorted_first(kind::StrategyKind, v, x[, hint]; order = Base.Order.Forward)
searchsorted_first(s, v, x[, hint]; order = Base.Order.Forward)FFF-owned positional search for the smallest index i with v[i] ≥ x under order (or v[i] ≤ x under Base.Order.Reverse). As with searchsorted_last, v must be sorted in ascending order under order (assumed, not checked). See searchsorted_last for the dispatch story.
FindFirstFunctions.strategy_kind — Function
strategy_kind(s::SearchStrategy) -> StrategyKindMap a singleton strategy struct to its enum tag. Auto returns its stored resolved kind. GuesserHint (genuinely stateful, no singleton tag) throws ArgumentError.
Kind ↔ strategy mapping
| Enum tag | Strategy struct | Kernel function |
|---|---|---|
KIND_BINARY_BRACKET | BinaryBracket | _kernel_last_binary_bracket / _kernel_first_binary_bracket |
KIND_LINEAR_SCAN | LinearScan | _kernel_last_linear_scan / _kernel_first_linear_scan |
KIND_SIMD_LINEAR_SCAN | SIMDLinearScan | _kernel_last_simd_linear_scan / _kernel_first_simd_linear_scan |
KIND_BRACKET_GALLOP | BracketGallop | _kernel_last_bracket_gallop / _kernel_first_bracket_gallop |
KIND_EXP_FROM_LEFT | ExpFromLeft | _kernel_last_exp_from_left / _kernel_first_exp_from_left |
KIND_INTERPOLATION_SEARCH | InterpolationSearch | _kernel_last_interpolation_search / _kernel_first_interpolation_search |
KIND_BIT_INTERPOLATION_SEARCH | BitInterpolationSearch | _kernel_last_bit_interpolation_search / _kernel_first_bit_interpolation_search |
KIND_UNIFORM_STEP | UniformStep | _kernel_last_uniform_step / _kernel_first_uniform_step |
KIND_BISECT_THEN_SIMD | BisectThenSIMD | (positional dispatch falls back to BinaryBracket; equality dispatch goes through findfirstsortedequal) |
Stateful strategies that do not have an enum tag and stay on the multimethod path:
Auto: carries aStrategyKindfield plus aSearchPropertiescache.Auto'ssearchsorted_lastis a one-line forward to the stored kind; the batched dispatch re-resolves the kind from(v, queries)because the gap heuristic needs the queries.GuesserHint: carries aGuesser(with itsidx_prev::Ref{Int}andlinear_lookup::Bool). Dispatches via its ownsearchsorted_last(::GuesserHint, ...)/searchsorted_first(::GuesserHint, ...)methods.
When to pick which
For most callers the answer is: pass Auto (the default in the batched API) and let it choose. The table below is for callers who already know their access pattern and want to pin a strategy.
| Strategy | Best when | Cost when hint hits | Cost worst case | Uses hint? |
|---|---|---|---|---|
LinearScan | answer is within a handful of slots of the hint | O(1) | O(n) | yes |
SIMDLinearScan | DenseVector{Int64} or DenseVector{Float64}, forward walk past the hint | O(1) | O(n/8) | yes |
BracketGallop | answer may be either side of the hint, distance unknown | O(1) | ~2 log₂ n | yes |
ExpFromLeft | sorted batch — hint is prev_result, answer is monotonically ≥ hint | O(1) | O(log n) | yes (as lower bound) |
InterpolationSearch | v is uniformly spaced and numeric | O(1) | O(log n) | no |
BitInterpolationSearch | DenseVector{Float64} and log-spaced (geometric) — opt-in, Auto does not pick | O(1) | O(log n) | no |
BinaryBracket | no hint available, or fallback | O(log n) | O(log n) | no |
UniformStep | v isa AbstractRange (or known-uniformly-spaced) | O(1) | O(1) | no |
GuesserHint | repeated correlated lookups against the same v | O(1) | ~2 log₂ n | self-provided |
Auto | unknown access pattern | varies | varies | yes if supplied |
All hint-consuming strategies fall back to BinaryBracket when no hint is supplied or when the hint is out of range. InterpolationSearch additionally falls back to BinaryBracket for non-numeric element types.
Migrating from v2 (Base.searchsortedlast(::S, ...)) to v3
The v2 Base.searchsortedlast(::S, ...) / Base.searchsortedfirst(::S, ...) methods are removed in v3. The migration is a mechanical rename — searchsortedlast → searchsorted_last, searchsortedfirst → searchsorted_first — with the strategy argument unchanged:
# v2 (removed)
searchsortedlast(BracketGallop(), v, x, hint)
searchsortedfirst(InterpolationSearch(), v, x)
searchsortedlast(Auto(v), v, x, hint)
searchsortedfirst(GuesserHint(g), v, x)
# v3
searchsorted_last(BracketGallop(), v, x, hint)
searchsorted_first(InterpolationSearch(), v, x)
searchsorted_last(Auto(v), v, x, hint)
searchsorted_first(GuesserHint(g), v, x)Hot loops that pick the strategy from runtime data should use the StrategyKind enum form (searchsorted_last(KIND_BRACKET_GALLOP, v, x, hint)); for a literal singleton strategy the struct form compiles to exactly the same code.
The batched API (searchsortedlast!, searchsortedfirst!, searchsortedrange) and the equality API (findequal, findfirstequal, findfirstsortedequal) are FFF-owned names and are unchanged.
Reference
FindFirstFunctions.LinearScan — Type
LinearScan <: SearchStrategyWalk ±1 from the hint. Best when the target is within a few positions of the hint. Falls back to BinaryBracket when no hint is supplied.
Maps to KIND_LINEAR_SCAN.
FindFirstFunctions.SIMDLinearScan — Type
SIMDLinearScan <: SearchStrategyVariant of LinearScan whose forward walk is lowered to 8-wide SIMD chunks via custom LLVM IR. Specialized for DenseVector{Int64} and DenseVector{Float64}; for any other element type, falls back to plain LinearScan. The backward walk (when the hint is past the answer) uses the scalar LinearScan path regardless of element type.
Maps to KIND_SIMD_LINEAR_SCAN.
Wins on long forward walks (≥ 8 elements past the hint). For walks of 1–3 elements LinearScan is comparable — the SIMD chunk has constant setup overhead. Worst case is O(n / 8) which is still linear, so SIMDLinearScan is only Auto-relevant for small n or small-gap batches where plain LinearScan would have been picked anyway.
Caveats:
- Element type must be exactly
Int64orFloat64. - Sorted-Float64 vectors containing
NaNproduce undefined results. - Falls back to
BinaryBracketwhen no hint is supplied. - Falls back to
LinearScanfor non-Forwardorderings.
FindFirstFunctions.BracketGallop — Type
BracketGallop <: SearchStrategyExpand an exponential bracket bidirectionally from the hint, then binary-search inside the bracket. Effectively O(1) when the target is near the hint; never worse than ~2 log₂ n comparisons.
Maps to KIND_BRACKET_GALLOP. Falls back to BinaryBracket when no hint is supplied.
FindFirstFunctions.ExpFromLeft — Type
ExpFromLeft <: SearchStrategyExponential search forward from the hint (interpreted as a left bound), then binary search in the final bracket. Best for batched sorted queries where each next query's hint is the previous result.
Maps to KIND_EXP_FROM_LEFT. Falls back to BinaryBracket when no hint is supplied.
FindFirstFunctions.InterpolationSearch — Type
InterpolationSearch <: SearchStrategyGuesses an index by linearly extrapolating x between v[lo] and v[hi], then refines with a bounded binary search.
Maps to KIND_INTERPOLATION_SEARCH. Ignores any hint. Falls back to BinaryBracket for non-numeric element types.
FindFirstFunctions.BitInterpolationSearch — Type
BitInterpolationSearch <: SearchStrategyVariant of InterpolationSearch that reinterprets DenseVector{Float64} as DenseVector{UInt64} before computing the extrapolation guess. Wins on log-spaced (geometric) data.
Maps to KIND_BIT_INTERPOLATION_SEARCH.
Constraints:
DenseVector{Float64}only.- Requires
v[1] > 0and the queryx > 0. - Forward / Reverse orderings only.
Opt-in only — Auto does not pick this strategy. Falls back to InterpolationSearch for non-Float64 dense eltypes, and to BinaryBracket for non-positive or non-finite Float64 data.
FindFirstFunctions.BinaryBracket — Type
BinaryBracket <: SearchStrategyPlain Base.searchsortedlast / Base.searchsortedfirst. Ignores any hint.
Maps to KIND_BINARY_BRACKET.
FindFirstFunctions.BisectThenSIMD — Type
BisectThenSIMD <: SearchStrategyEquality-search strategy. Binary-bisects v down to a small basecase, then SIMD-scans the basecase for exact equality with x. Specialised for DenseVector{Int64} + Int64 queries.
Maps to KIND_BISECT_THEN_SIMD. Meant for use with findequal, not with searchsortedfirst / searchsortedlast — in the positional API it falls back to BinaryBracket.
FindFirstFunctions.UniformStep — Type
UniformStep <: SearchStrategyO(1) direct-arithmetic lookup for uniformly-spaced vectors.
Maps to KIND_UNIFORM_STEP. Specialized for AbstractRange{<:Real}; for other vector types, falls back to BinaryBracket. Ignores any hint.
FindFirstFunctions.Auto — Type
Auto <: SearchStrategy
Auto()
Auto(props::SearchProperties)
Auto(v::AbstractVector)
Auto(v::AbstractVector, props::SearchProperties)Stateful strategy that resolves to a concrete StrategyKind at construction time. The resolution uses static information available at construction: props (if supplied) plus v (if supplied).
Per-query (searchsorted_last(Auto(), v, x[, hint])): forwards directly to the stored kind. Auto() defaults to KIND_BINARY_BRACKET (safe choice when nothing is known about v); Auto(v) resolves to a faster kind based on length(v), props.is_uniform, etc. Callers that want the v2-era "pick at every query based on length and hint" behaviour should explicitly construct Auto(v) for each new v.
Batched sorted (searchsortedlast!(out, v, queries; strategy = Auto())): the batched dispatcher re-resolves the kind from (v, queries) even when Auto() carries the default kind — the gap-based decision tree requires the queries, which aren't available at Auto-construction time.
Cached properties. Passing a populated SearchProperties via Auto(props) or Auto(v, props) short-circuits the per-call probes. The cached path is behaviour-equivalent to Auto(v) when props is up to date for v; the caller is responsible for re-computing props if v mutates.
Fields
kind::StrategyKind— resolved kind. Use this field directly in hot loops viasearchsorted_last(auto.kind, v, x, hint)to skip theauto.propsfield load entirely.props::SearchProperties— cached properties used by the batched decision tree.
FindFirstFunctions.SearchProperties — Type
SearchProperties{T}Cached, non-allocating facts about a sorted vector. Pass to Auto via Auto(props) to skip the per-call probes that the default Auto() runs on every batched call.
Default-constructed (SearchProperties()) is the "no information" sentinel: has_props is false, the other fields are unspecified and ignored by Auto. Construct via SearchProperties(v::AbstractVector) to populate the fields by running the probes once.
T is the data ratio type — the type of oneunit(eltype(v)) / oneunit(eltype(v)), so e.g. SearchProperties{Float64} for Vector{Int} (because Int/Int promotes to Float64) or Vector{Float64}, SearchProperties{Float32} for Vector{Float32}. For non-Number eltypes the default is Float64 and has_props = false.
Currently consumed by Auto:
is_linear— gatesInterpolationSearchin batched dispatch.has_nan(Float64 only) — gatesSIMDLinearScaneligibility.is_uniform— short-circuits toUniformStepwhen set, withfirst_valandinv_stepbaked in for closed-form O(1) lookup.
When is_uniform = true, first_val and inv_step hold the precomputed data needed by UniformStep's closed-form path (idx = floor((x - first_val) * inv_step) + 1). When is_uniform = false they are zero(T) and never consulted.
The is_log_linear field is populated for callers that want to manually pin BitInterpolationSearch; Auto does not consume it.
Equality search through the strategy framework
Strategies answer positional questions ("where would x insert?"). Equality asks a different question ("is x at exactly which index?"). The findequal wrapper builds the latter on top of the former: every strategy gets an equality variant for free.
FindFirstFunctions.findequal — Function
findequal(strategy, v, x[, hint]; order = Base.Order.Forward) -> IntReturn the index of x in sorted v if present, or the sentinel firstindex(v) - 1 if x is absent. Type-stable Int return — the sentinel matches the convention Base.searchsortedlast already uses for "x precedes all of v".
The strategy argument can be:
- A singleton strategy struct (
BinaryBracket(),BracketGallop(), …) — forwards through the struct → kind mapping. - A
StrategyKindenum value (KIND_BRACKET_GALLOP, …). - A stateful strategy (
Auto,GuesserHint) — dispatched via multimethod.
Most strategies are handled generically. The shortcut method on BisectThenSIMD for DenseVector{Int64} skips the searchsortedfirst path entirely.
For unsorted vectors, use findfirstequal.
The sentinel for "not found" is firstindex(v) - 1 (= 0 for 1-based vectors). Type-stable Int return, no Union with Nothing. Callers can test for absence with i < firstindex(v).
findequal routes most strategies through searchsorted_first + post-check generically, so findequal(BracketGallop(), v, x, hint), findequal(SIMDLinearScan(), v, x, hint), findequal(GuesserHint(g), v, x), findequal(KIND_BRACKET_GALLOP, v, x, hint), etc. all just work.
The BisectThenSIMD strategy short-circuits the post-check path on DenseVector{Int64} by dispatching into findfirstsortedequal directly — same custom LLVM IR scan, exposed through the strategy framework.
GuesserHint is documented on the Guessers page.
Notes on individual strategies
LinearScan
Walks ±1 from the hint until the answer is bracketed. Cheapest possible search when the hint is right next to the answer — two comparisons. The only strategy whose worst case is O(n), so it should only be picked when the caller has strong evidence that the hint is close.
For length(v) ≤ 16, LinearScan is faster than BracketGallop even from a bad hint because the bracket bookkeeping costs more than a worst-case walk across a vector that short. Auto's resolution rule picks KIND_LINEAR_SCAN below that threshold.
SIMDLinearScan
Same algorithm as LinearScan, with the forward walk past the hint lowered to 8-wide SIMD chunks via custom LLVM IR. The backward walk (when the hint is past the answer) uses the scalar LinearScan path — the SIMD primitive is only defined in the forward direction.
Specialized for DenseVector{Int64} and DenseVector{Float64}. Any other element type falls back to the scalar LinearScan walk. The dispatch is static — there's no runtime type test on a hot path.
Caveats:
- Element types:
Int64andFloat64only. - NaN: a
NaNelement in aFloat64vector compares asfalse— the SIMD scan silently skips it. SortedFloat64vectors containingNaNaren't well-defined under any total order. - Order:
ForwardandReverseonly. - No hint: falls back to
BinaryBracket. - Auto does not pick this strategy by default in the per-query path. The batched dispatch picks it inside a gap window where the SIMD chunk pays for itself.
BitInterpolationSearch
InterpolationSearch with the extrapolation guess computed on the IEEE bit pattern of v rather than the float values themselves. Wins on log-spaced (geometric) data — sometimes O(1) versus O(log n) refinement cost.
Opt-in only.Auto does not pick BitInterpolationSearch.
Falls back to plain InterpolationSearch on non-Float64 dense eltypes, and to BinaryBracket for non-positive or non-finite Float64 data.
BracketGallop
Galloping search around the hint: expand [lo, hi] outward by doubling steps until x is bracketed, then binary-search inside [lo, hi]. Direction is inferred from v[hint] vs. x, so the hint can be either above or below the answer. Worst case is ~2 log₂ n — about twice plain binary search — and it matches O(1) when the hint is close.
This is the workhorse hinted strategy and the natural choice for get_idx-style callers where the hint is a cached previous result.
ExpFromLeft
Exponential search forward from a hint interpreted as a lower bound. The algorithm probes v[lo], v[lo+1], …, v[lo+4] linearly, then v[lo+8], v[lo+16], … exponentially, then binary-searches inside the final bracket.
Used by Auto's batched dispatch when the queries are sorted: each call passes hint = previous_result, which by sortedness satisfies the "answer ≥ hint" precondition.
InterpolationSearch
Computes a guess via linear extrapolation between v[lo] and v[hi], then refines with a bounded binary search around that guess. On uniformly-spaced numeric data the first guess is the right answer — O(1) per query independent of n.
Two restrictions:
- Numeric eltype: non-numeric eltypes fall back to
BinaryBracket. - Forward ordering only: non-
Forwardorderings fall back toBinaryBracket.
The hint is ignored — the guess is computed fresh from the endpoints.
BinaryBracket
Plain Base.searchsortedlast / Base.searchsortedfirst. Provided as a strategy so callers can opt out of hint-based behaviour explicitly, and so other strategies have a well-defined name to fall back to. Ignores any hint.
Auto
See Auto: heuristics and benchmarks for the full decision tree and the benchmark sweep that produced its crossover constants.
In v3, Auto carries a stored StrategyKind plus a SearchProperties cache:
Auto()defaults toKIND_BINARY_BRACKET. Safe but no faster than plainBase.searchsortedlast.Auto(v)resolves the kind fromlength(v)andSearchProperties(v). PicksKIND_UNIFORM_STEPforAbstractRange/ detected-uniform vectors,KIND_LINEAR_SCANfor short vectors,KIND_BRACKET_GALLOPotherwise.Auto(v, props)is the same with a pre-computedpropscache.
The per-query searchsorted_last(::Auto, v, x, hint) is a one-line forward to searchsorted_last(s.kind, v, x, hint). The batched searchsortedlast!(out, v, queries; strategy = Auto()) re-resolves the kind from (v, queries) to consult the gap heuristic.
Equality routines
The package exposes two Union{Int, Nothing}-returning equality routines — findfirstequal (unsorted SIMD scan) and findfirstsortedequal (sorted bisect-then-SIMD scan). See the Equality search page for the full documentation.