Search Policies
This page explains each search policy in detail, including when to use it and how it works internally.
AutoSearch (Default) — Recommended for All Use Cases
Automatically selects BinarySearch() or LinearBinarySearch() based on query type, hint presence, and data access pattern at call time. This is the default for all interpolants — no configuration needed.
AutoSearch() adapts per-call to your actual query pattern. It handles random, sorted, streaming, and mixed patterns correctly with near-optimal performance. You only need to set a search policy explicitly if you have benchmarked and confirmed a measurable benefit.
Resolution rules:
| Query type | Hint | Resolved policy | Rationale |
|---|---|---|---|
Scalar (Real) | none | BinarySearch() | Stateless; no locality to exploit |
Scalar (Real) | Ref{Int} | → LinearBinarySearch() | Auto-upgraded: hint implies sequential pattern (e.g. SeriesInterpolant) |
Vector (AbstractVector) | none | BinarySearch() or LinearBinarySearch() | Adaptive: prefix monotonicity check on first K=8 elements |
Vector (AbstractVector) | Ref{Int} | LinearBinarySearch() | Caller has walk state, expects locality |
ND scalar (NTuple{N,Real}) | none | BinarySearch() per axis | Same as 1D scalar |
ND SoA batch (NTuple{N,AbstractVector}) | none | BinarySearch() or LinearBinarySearch() per axis | Adaptive per axis |
Broadcast (itp.(xs)) | — | BinarySearch() per element | Each broadcast call is independent |
itp = linear_interp(x, y) # stores AutoSearch (the default)
itp(0.5) # → BinarySearch() (scalar query)
itp(sorted_xs) # → LinearBinarySearch() (sorted detected)
itp(rand_xs) # → BinarySearch() (random detected)
itp.(xs) # → BinarySearch() per element (broadcast)
# Hint auto-upgrade (scalar + hint → LinearBinarySearch)
hint = Ref(1)
itp(0.5; hint=hint) # → LinearBinarySearch() (hint implies locality)
# Override when you know the pattern in advance:
itp(0.5; search=BinarySearch()) # force BinarySearch
itp(sorted_xs; search=LinearBinarySearch()) # force LinearBinarySearchHow it works: AutoSearch is resolved in two phases at the call site, not at construction time:
- Policy resolution (
_resolve_search): PicksBinarySearchorLinearBinarySearchbased on query type and, for vector queries without a hint, a prefix monotonicity check on the first 8 elements. - Searcher creation (
_to_searcher): When aRef{Int}hint is provided alongsideBinarySearch, it auto-upgrades toLinearBinarySearch— because the hint implies the caller expects walk-based locality (e.g.,SeriesInterpolantscalar evaluation).
When to override with an explicit policy:
- You know queries are always random → explicit
BinarySearch()skips the adaptive check (marginal benefit) - You know queries are always sorted → explicit
LinearBinarySearch()skips the adaptive check (marginal benefit) - For most use cases, keeping
AutoSearch()is the right choice — the adaptive overhead is negligible
BinarySearch
Pure binary search. Stateless, thread-safe, no setup required.
Complexity: O(log n) per query
When to use:
- Random access patterns
- One-off queries
- When you want consistent O(log n) for any query order
itp = linear_interp(x, y)
val = itp(0.5) # AutoSearch → BinarySearch() for scalar
val = itp(0.5; search=BinarySearch()) # explicit BinarySearchHow it works: Standard binary search on the grid. Each query starts fresh with no memory of previous queries.
LinearBinarySearch
Performs a bounded linear search from the hint position, with automatic binary fallback. This is the safe explicit choice for sorted or monotonic queries — and what AutoSearch resolves to when it detects sorted data.
Complexity: O(1) within the linear window, O(log n) fallback
When to use:
- Sorted query sequences
- Monotonically increasing time (ODE solvers)
- Streaming data with local continuity
- Any case where you'd consider
LinearSearch()—LinearBinarySearch()is almost always the better choice
sorted_queries = sort(rand(1000))
itp = linear_interp(x, y; search=LinearBinarySearch())
vals = itp(sorted_queries) # O(1) amortized for sorted inputHow it works: Starting from the hint position, walks linearly left or right (up to linear_window). If the target interval isn't found within bounds, falls back to binary search. This fallback guarantees O(log n) worst-case — making it safe for any query pattern.
Tuning linear_window
You can tune the linear search window size before falling back to binary search:
LinearBinarySearch() # default: linear_window=8
LinearBinarySearch(linear_window=0) # hint check only, no walk (minimal random overhead)
LinearBinarySearch(linear_window=4) # narrow window for tight jitter patterns
LinearBinarySearch(linear_window=16) # wider window for sparser-spaced sorted queriesGuidelines:
- Zero (0): Hint check only, no walk — minimal random-query overhead. Good when queries cluster in the same interval.
- Small
linear_window(1–2): Minimal overhead for mixed query patterns - Medium values (4): Good for narrow jitter patterns (step size < 2 intervals)
- Default (8): Best balance — +2.5ns random overhead, covers jitter up to ~6 intervals
- Large
linear_window(16–128): For wide jitter, highly localized queries, or very large datasets
linear_window is restricted to 0 plus powers of 2 (1, 2, 4, 8, 16, 32, 64, 128) to prevent type parameter explosion. Each unique value creates a specialized method, so limiting choices keeps compile times reasonable.
LinearGallopSearch (Opt-in)
Same direct hit and linear walk as LinearBinarySearch, but when the target lies beyond the window it gallops outward from where the walk stopped (1, 2, 4, 8, … cells) and bisects that bracket, instead of restarting a binary search over the whole grid. This is also known as exponential search, or hunt in Numerical Recipes. AutoSearch never selects it.
Complexity: O(1) within the linear window, O(log gap) beyond it, where gap is the number of cells between consecutive queries
When to use — sorted or local queries that are a moderate distance apart:
- Sampling a long, fine grid at a coarser set of sorted points, e.g. about 10,000–100,000 sorted queries on a 1M-point grid
- Streaming queries with a hint whose steps are tens to hundreds of cells
When not to use — keep the default:
- Random access: galloping across a large part of the grid costs about twice the probes of a binary search
- Very sparse sorted queries (gaps ≳ √n cells), e.g. 10–1,000 queries on a 1M-point grid
- Dense sorted queries (gaps within the window): nothing to gain
itp = linear_interp(x, y)
vals = itp(sorted_queries; search=LinearGallopSearch()) # call-site override
itp = linear_interp(x, y; search=Search(:linear_gallop)) # baked in
LinearGallopSearch(linear_window=16) # same curated windows as LinearBinarySearchTime ratio LinearGallopSearch / LinearBinarySearch for itp(out, xq) with sorted queries spread over a non-uniform grid (below 1 = faster; Apple M1 Pro):
| grid points \ queries | 10 | 100 | 1,000 | 10,000 | 100,000 | 1,000,000 |
|---|---|---|---|---|---|---|
| 100 | 0.80 | 1.04 | 1.01 | 1.00 | 1.00 | 1.00 |
| 1,000 | 1.34 | 0.60 | 1.01 | 1.10 | 1.00 | 1.00 |
| 10,000 | 1.52 | 0.97 | 0.46 | 0.99 | 1.01 | 1.00 |
| 100,000 | 1.66 | 1.59 | 0.77 | 0.31 | 1.01 | 1.00 |
| 1,000,000 | 2.23 | 1.95 | 1.20 | 0.70 | 0.44 | 1.00 |
The gain sits in a band where the average gap is roughly 10–100 cells. A full-range binary search costs log₂(n) probes wherever the target is, and its first probes hit the same cache-hot nodes for every query; galloping costs about 2·log₂(gap) probes from a different starting point each time, so it only pays off well below √n cells.
LinearSearch (Expert Only)
LinearSearch() has no binary fallback. If queries are far apart, jump around, or reverse direction, it walks the entire grid — degrading to O(n) per query. This can be orders of magnitude slower than BinarySearch() or LinearBinarySearch().
Use LinearBinarySearch() instead — it provides the same O(1) performance for close, monotonic queries but with a safe O(log n) fallback when things go wrong.
Pure linear search for strictly monotonic, closely-spaced queries. Scans the grid one interval at a time from the hint until the target is found — no fallback, no window limit.
Complexity: O(1) amortized for monotonic, closely-spaced sequences; O(n) worst case
The only case where LinearSearch() may be justified:
- Tight inner loops where every nanosecond matters
- Queries are guaranteed to be strictly monotonic and closely-spaced (e.g., fixed-step ODE with small dt)
- You have benchmarked and confirmed a measurable advantage over
LinearBinarySearch()
When NOT to use (use LinearBinarySearch() or AutoSearch() instead):
- Random access patterns
- Queries that may be far apart
- Large grids with sparse query spacing
- Adaptive-step ODE solvers (step size can vary widely)
- Any general-purpose code
# ONLY if you have benchmarked and confirmed this is faster for your specific case
itp = linear_interp(x, y)
hint = Ref(1)
for t in t_values # MUST be strictly monotonic and closely-spaced
y = itp(t; search=LinearSearch(), hint=hint)
end
# Safer alternative that is equally fast for monotonic data:
for t in t_values
y = itp(t; search=LinearBinarySearch(), hint=hint)
endHow it works: Walks linearly from the hint position one interval at a time. Without a step limit, it will traverse the entire grid if necessary — which is optimal for close queries but catastrophic for distant ones.
Performance Summary
| Query Pattern | AutoSearch() | BinarySearch() | LinearBinarySearch{8}() | LinearSearch() |
|---|---|---|---|---|
| Random | ✅ Same as BinarySearch() | ✅ Best | ~2-3x slower | ❌ Up to O(n) — dangerous |
| Sorted | ✅ Same as LinearBinarySearch() | Baseline | ✅ ~5x faster (~2 ns) | ✅ ~5x faster |
| Jittered | ✅ Same as LinearBinarySearch() | Baseline | ✅ ~2-5x faster | ⚠️ Unpredictable |
AutoSearch() adapts to your data: it detects sorted queries and uses LinearBinarySearch, detects random queries and falls back to BinarySearch. You get near-optimal performance for both patterns without choosing manually.
AutoSearch() gives you near-optimal performance for every query pattern. The only reason to set an explicit policy is to skip the adaptive prefix check — which saves a few nanoseconds at most.
Approximate results from vector batch calls for a specific test case. Run benchmarks with your own data for accurate numbers.
Baked-in vs Override
Baked-in Policy (Constructor)
Override the default AutoSearch when creating the interpolant. The stored policy applies to all subsequent calls:
itp = linear_interp(x, y) # AutoSearch() — adapts per call
itp = linear_interp(x, y; search=BinarySearch()) # always BinarySearch()
itp = linear_interp(x, y; search=LinearBinarySearch()) # always LinearBinarySearch()Override at Call Time
Override the stored policy for a single call without changing the interpolant:
itp = linear_interp(x, y) # stores AutoSearch (default)
# Call-site override — does not change itp.search_policy
val = itp(0.5; search=BinarySearch()) # force BinarySearch for this call
vals = itp(sorted_xs; search=LinearBinarySearch()) # force LinearBinarySearch for this callThis is useful when most queries benefit from one policy, but occasional queries need different behavior.
See Also
- Overview — Quick decision matrix
- Using Hints — External hints for maximum performance