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 StrategyKind enum value (e.g. KIND_BRACKET_GALLOP). One enum value per singleton strategy; runtime if/elseif dispatch 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 through strategy_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.SearchStrategyType
SearchStrategy

Abstract 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 matching StrategyKind enum value; pass either the struct or its enum tag to searchsorted_last / searchsorted_first. The struct form forwards through strategy_kind and 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 own searchsorted_last / searchsorted_first multimethods.

Strategies can also be passed to the batched searchsortedlast! / searchsortedfirst! APIs.

source
FindFirstFunctions.StrategyKindType
StrategyKind

Enum 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_GALLOPBracketGallop). 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.

source
FindFirstFunctions.searchsorted_lastFunction
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.

source
FindFirstFunctions.searchsorted_firstFunction
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.

source
FindFirstFunctions.strategy_kindFunction
strategy_kind(s::SearchStrategy) -> StrategyKind

Map a singleton strategy struct to its enum tag. Auto returns its stored resolved kind. GuesserHint (genuinely stateful, no singleton tag) throws ArgumentError.

source

Kind ↔ strategy mapping

Enum tagStrategy structKernel function
KIND_BINARY_BRACKETBinaryBracket_kernel_last_binary_bracket / _kernel_first_binary_bracket
KIND_LINEAR_SCANLinearScan_kernel_last_linear_scan / _kernel_first_linear_scan
KIND_SIMD_LINEAR_SCANSIMDLinearScan_kernel_last_simd_linear_scan / _kernel_first_simd_linear_scan
KIND_BRACKET_GALLOPBracketGallop_kernel_last_bracket_gallop / _kernel_first_bracket_gallop
KIND_EXP_FROM_LEFTExpFromLeft_kernel_last_exp_from_left / _kernel_first_exp_from_left
KIND_INTERPOLATION_SEARCHInterpolationSearch_kernel_last_interpolation_search / _kernel_first_interpolation_search
KIND_BIT_INTERPOLATION_SEARCHBitInterpolationSearch_kernel_last_bit_interpolation_search / _kernel_first_bit_interpolation_search
KIND_UNIFORM_STEPUniformStep_kernel_last_uniform_step / _kernel_first_uniform_step
KIND_BISECT_THEN_SIMDBisectThenSIMD(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 a StrategyKind field plus a SearchProperties cache. Auto's searchsorted_last is 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 a Guesser (with its idx_prev::Ref{Int} and linear_lookup::Bool). Dispatches via its own searchsorted_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.

StrategyBest whenCost when hint hitsCost worst caseUses hint?
LinearScananswer is within a handful of slots of the hintO(1)O(n)yes
SIMDLinearScanDenseVector{Int64} or DenseVector{Float64}, forward walk past the hintO(1)O(n/8)yes
BracketGallopanswer may be either side of the hint, distance unknownO(1)~2 log₂ nyes
ExpFromLeftsorted batch — hint is prev_result, answer is monotonically ≥ hintO(1)O(log n)yes (as lower bound)
InterpolationSearchv is uniformly spaced and numericO(1)O(log n)no
BitInterpolationSearchDenseVector{Float64} and log-spaced (geometric) — opt-in, Auto does not pickO(1)O(log n)no
BinaryBracketno hint available, or fallbackO(log n)O(log n)no
UniformStepv isa AbstractRange (or known-uniformly-spaced)O(1)O(1)no
GuesserHintrepeated correlated lookups against the same vO(1)~2 log₂ nself-provided
Autounknown access patternvariesvariesyes 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 — searchsortedlastsearchsorted_last, searchsortedfirstsearchsorted_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.SIMDLinearScanType
SIMDLinearScan <: SearchStrategy

Variant 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 Int64 or Float64.
  • Sorted-Float64 vectors containing NaN produce undefined results.
  • Falls back to BinaryBracket when no hint is supplied.
  • Falls back to LinearScan for non-Forward orderings.
source
FindFirstFunctions.BracketGallopType
BracketGallop <: SearchStrategy

Expand 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.

source
FindFirstFunctions.ExpFromLeftType
ExpFromLeft <: SearchStrategy

Exponential 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.

source
FindFirstFunctions.InterpolationSearchType
InterpolationSearch <: SearchStrategy

Guesses 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.

source
FindFirstFunctions.BitInterpolationSearchType
BitInterpolationSearch <: SearchStrategy

Variant 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] > 0 and the query x > 0.
  • Forward / Reverse orderings only.

Opt-in onlyAuto 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.

source
FindFirstFunctions.BisectThenSIMDType
BisectThenSIMD <: SearchStrategy

Equality-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.

source
FindFirstFunctions.UniformStepType
UniformStep <: SearchStrategy

O(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.

source
FindFirstFunctions.AutoType
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 via searchsorted_last(auto.kind, v, x, hint) to skip the auto.props field load entirely.
  • props::SearchProperties — cached properties used by the batched decision tree.
source
FindFirstFunctions.SearchPropertiesType
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 — gates InterpolationSearch in batched dispatch.
  • has_nan (Float64 only) — gates SIMDLinearScan eligibility.
  • is_uniform — short-circuits to UniformStep when set, with first_val and inv_step baked 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.

source

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.findequalFunction
findequal(strategy, v, x[, hint]; order = Base.Order.Forward) -> Int

Return 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 StrategyKind enum 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.

source

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: Int64 and Float64 only.
  • NaN: a NaN element in a Float64 vector compares as false — the SIMD scan silently skips it. Sorted Float64 vectors containing NaN aren't well-defined under any total order.
  • Order: Forward and Reverse only.
  • 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-Forward orderings fall back to BinaryBracket.

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 to KIND_BINARY_BRACKET. Safe but no faster than plain Base.searchsortedlast.
  • Auto(v) resolves the kind from length(v) and SearchProperties(v). Picks KIND_UNIFORM_STEP for AbstractRange / detected-uniform vectors, KIND_LINEAR_SCAN for short vectors, KIND_BRACKET_GALLOP otherwise.
  • Auto(v, props) is the same with a pre-computed props cache.

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.