Performance Tips
FastInterpolations.jl is fast by default — for most code you do not need to tune anything. This page is an advanced-user checklist for the cases where you do want the last few nanoseconds, in rough order of impact. Each lever links to the guide with the full details, and to the Benchmarks page for measured numbers.
At a Glance
| Lever | One-liner |
|---|---|
| Grid type | Represent a uniform grid with the most specific Range type that is still correct. |
| Batch queries | Pass a vector; never loop over scalar calls. Use the ! in-place form in hot loops. |
GriddedQuery | For tensor-product ND queries, pass one query axis per dimension and write into shaped output. |
| Search hints | For sequential access, pass hint = Ref(1). |
InBounds | When queries are known in-domain, skip the domain check. |
value_gradient | Need value and gradient? Locate the interval once. |
Choose the Leanest Grid Type
The grid's type — not just its values — decides how much work each query spends locating its interval. Represent a uniform grid with the most specific Range type that is still correct. This is a correctness-preserving choice, not a free speed dial: swapping 0.0:0.1:10.0 for 1:100 changes the coordinates, so it only applies when your grid genuinely has that shape.
For a grid whose points are 1, 2, …, length(y), worst to best:
x = collect(1.0:length(y)) # ❌ Vector — O(log n) binary search
x = 1.0:length(y) # ⚠️ Float range — O(1), but the index needs a ×(1/step)
x = 1:length(y) # ✅ UnitRange — Int, step ≡ 1 pinned at the type level
x = Base.OneTo(length(y)) # ✅ OneTo — also 1-based at the type level
x = axes(y, 1) # ✅ idiomatic — a vector's own axis *is* a `Base.OneTo`Each step down the list pins more of the grid's structure into the type, so the compiler drops work from the per-query index math: the Vector loses O(1) lookup entirely, a float range carries a runtime reciprocal, a UnitRange fixes the step to 1, and Base.OneTo fixes the origin to 1 so the interval index is the query position (index-space search).
Pick a row, then use it the same way — build once or one-shot:
linear_interp(x, y) # persistent interpolant
linear_interp(x, y, xq) # one-shot
linear_interp(axes(y), y) # `axes(y)` is the natural whole-array form (routes to 1-D)The gains are small and matter only in tight scalar/batch loops — but they cost nothing when the grid genuinely has that shape (samples on 1, 2, …, n). Use 1:n / Base.OneTo(n) rather than Float64.(1:n) or collect(1:n) when the x-coordinate is the index.
1:n keeps the unit-step fast path but not the type-level 1-based one — its first == 1 is a runtime value, and promoting on it would make the interpolant type value-dependent. The search still detects the lo == 1 case at run time, so 1:n gets most of the benefit; Base.OneTo(n) makes it a type-level guarantee. See Grid Selection for the basic Range-vs-Vector trade-off.
For non-uniform grids a Vector is required — see the next levers and Search & Hints to optimize its lookup.
Batch Queries and In-place Output
When querying multiple points, pass the whole vector — the batch path amortizes per-call overhead that a scalar loop pays on every iteration:
# ❌ per-call overhead on every element
out = [linear_interp(x, y, q) for q in xq]
# ✅ batch path
out = linear_interp(x, y, xq)In a hot loop, pre-allocate the output once and use the ! in-place form for true zero-allocation:
out = similar(xq)
linear_interp!(out, x, y, xq) # reuses `out`, no per-call allocationSee Memory & Allocation for the full treatment.
Use GriddedQuery for Tensor-Product ND Batches
The usual ND batch query (xqs, yqs) evaluates pairwise points: (xqs[i], yqs[i]) for each i, and returns a vector. For image resize, coarse-to-fine resampling, or any rectilinear output grid where you want every combination of one query axis per dimension, wrap the axes in GriddedQuery:
itp = linear_interp((x, y), data)
xq = range(first(x), last(x), 512)
yq = [first(y), (first(y) + last(y)) / 2, last(y)]
# Slow pattern: one scalar call per output point.
naive = [itp((xi, yi)) for xi in xq, yi in yq]
# Fast pattern: resolve each query axis once, then reuse it across the output grid.
gq = GriddedQuery(xq, yq) # Range axis + Vector axis; no collect needed
img = itp(gq) # size(img) == (512, 3)
isapprox(img, naive) # same values, much less workIn hot loops, pre-allocate an N-D output whose size exactly matches size(gq):
img_oneshot = linear_interp((x, y), data, gq) # allocating one-shot path
out = Matrix{Float64}(undef, size(gq))
itp(out, gq) # persistent path
linear_interp!(out, (x, y), data, gq) # one-shot linear pathFor method-selected one-shot calls such as cubic or quadratic, use the unified interp / interp! API so the shaped GriddedQuery output reaches the gridded fast path:
cubic_img = interp((x, y), data, gq; method = CubicInterp(), extrap = ClampExtrap())
interp!(out, (x, y), data, gq; method = CubicInterp(), extrap = ClampExtrap())GriddedQuery accepts any AbstractVector query axis, including AbstractRange. Keeping range axes as ranges preserves their O(1) lookup behavior and avoids an unnecessary allocation. For N > 1, the in-place output must be an N-D array; a flat vector is not reshaped automatically.
Search Hints for Sequential Access
On Vector grids the interval search matters (uniform Range grids are already O(1)). The default AutoSearch() adapts per call and suits almost everyone. For sequential or streaming access (ODE time-stepping, sorted scans), a hint = Ref(1) is the single most effective lever — it upgrades scalar queries to O(1) walk-based search with a safe O(log n) fallback:
itp = linear_interp(x, y)
hint = Ref(1)
for t in monotonic_times
itp(t; hint = hint) # O(1) amortized
endSee Search & Hints for the decision matrix and thread-safety patterns.
The InBounds Fast Path
When you already know a query lies inside the grid domain, skip the extrapolation domain check with InBounds() — the interpolation analogue of Julia's @inbounds. For a batch this elides the whole O(n) in-domain scan; for a scalar it removes a compare-and-branch.
# One-shot form (always supported):
linear_interp(x, y, xq; extrap = InBounds())
# Persistent interpolant — override per call, without rebuilding:
itp = cubic_interp(x, y; extrap = NoExtrap()) # stored contract
itp(xq) # runs the domain check
itp(xq; extrap = InBounds()) # skips it (this call only)The stored extrapolation contract is unchanged — InBounds is a per-call assertion, not a new extrapolation mode, so it composes onto any stored extrap (including periodic WrapExtrap).
For strictly-interior queries on unit-step range grids, the narrower InBounds(last = :exclusive) promises first(x) ≤ xq < last(x) and unlocks an even leaner direct-index search:
itp((xq, yq, zq); extrap = InBounds(last = :exclusive))Like @inbounds, violating the promise is undefined behavior — an actually out-of-domain query reads outside the grid. Only opt in when the caller guarantees the query is in-domain. See Extrapolation for the full contract table.
Value and Gradient Together
In optimization inner loops you often need both the value and the gradient at a point. value_gradient performs the interval search once and returns both, instead of paying for two separate locates — the ideal match for Optim.jl's only_fg! interface:
val, grad = value_gradient(itp, (x, y)) # one locate, both outputsSee Optimization for the full fg! pattern.
See Also
- Memory & Allocation — grid choice, batch API, zero-alloc
- Search & Hints — search policies and hint patterns
- Extrapolation —
InBoundsand the full extrapolation contract - Multi-Dimensional Interpolation — ND query modes and
GriddedQuery - Optimization (Optim.jl) —
value_gradient, analytical derivatives - Benchmarks — measured comparisons against Interpolations.jl