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