Add LinearBinarySearch{MAX}: bounded linear walk + binary fallback#72
Draft
ChrisRackauckas-Claude wants to merge 1 commit into
Draft
Add LinearBinarySearch{MAX}: bounded linear walk + binary fallback#72ChrisRackauckas-Claude wants to merge 1 commit into
ChrisRackauckas-Claude wants to merge 1 commit into
Conversation
LinearBinarySearch{MAX} walks linearly from the hint for up to MAX steps,
then falls back to BinaryBracket if the answer isn't bracketed within the
window. The win regime is small-gap ODE-style monotone-forward workloads
where the exponential-doubling overhead of ExpFromLeft / BracketGallop is
pure cost and a tight unrolled linear walk is fastest.
Default MAX = 8; allowed values are {0, 1, 2, 4, 8, 16, 32, 64, 128} via a
factory constructor. The set is curated to keep the per-MAX method
specialization table bounded; arbitrary integers would explode it. MAX is
a type parameter so the walk is fully unrolled (for MAX ≤ 16) via
@generated, producing flat branchless-ish code that LLVM can fold.
Bench (bench/linearbinary_sweep.jl, n = 100k Float64, ns/query):
gap LBS{4} LinearScan ExpFromLeft BracketGallop
1 9.6 10.0 11.7 17.0
2 11.0 11.0 12.8 20.5
4 14.0 14.0 22.0 23.5
8 43.0 19.0 22.7 25.0 ← MAX exceeded → fallback
16 42.0 30.0 26.0 29.5
64 42.0 101.0 33.0 39.5
LBS{4} wins by ~0.5–1 ns/q at gap = 1; ties LinearScan at gap = 2. At
gaps beyond MAX, the binary fallback caps the worst case at O(log n).
The strategy is opt-in — Auto does not pick it, because the gap=1 win is
too marginal for a runtime heuristic to recoup.
Includes tests covering: factory constructor validation, parity-vs-Base
across MAX ∈ {0, 1, 2, 4, 8, 16, 32, 64, 128}, Forward and Reverse
orderings, hint past / below / at the answer, gap = MAX vs gap = MAX+1
boundary, empty / n=1 / n=1M, duplicates, and no-hint fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
LinearBinarySearch{MAX} <: SearchStrategyto FFF: walk linearly from the hint for up toMAXsteps, fall back to a binary search if the answer isn't bracketed within the window. Mirrors the strategy of the same name in FastInterpolations.jl; FFF previously hadExpFromLeft(forward exponential doubling) andBracketGallop(bidirectional doubling) but no bounded-linear-with-binary-fallback option.The intended workload is small-gap ODE-style monotone-forward sweeps where the exponential-doubling overhead of
ExpFromLeft/BracketGallopis pure cost and a tight unrolled linear walk is fastest.Design choices
MAX = 8, matching FastInterpolations.jl. Allowed values are{0, 1, 2, 4, 8, 16, 32, 64, 128}via a factory constructor — curated to keep the per-MAXmethod specialization table bounded. Arbitrary integers via the parametric formLinearBinarySearch{k}()still work but won't go through validation.MAXis a type parameter, so the walk is fully unrolled. ForMAX ≤ 16an@generatedfunction produces flat branchless-ish code; forMAX > 16the walk falls back to a bounded while-loop (unrolling at 128 would balloon the code size).Base.Order.lton the predicate, soForwardandReverseorderings share one code path.BinaryBracket.MAXcovers gaps0..MAX(initial gap-0 check plus MAX advance-and-check pairs).Bench (bench/linearbinary_sweep.jl, n = 100k Float64, ns/query)
LinearBinarySearch{4}wins by ~0.5–1 ns/q at gap = 1 and tiesLinearScanat gap = 2. At gaps beyondMAX, the binary fallback caps the worst case at O(log n) — slightly worse thanBracketGallopat large gaps but bounded.Auto integration decision: opt-in only
The gap=1 win is too marginal (~1 ns/q) for a runtime
Autoheuristic to recoup — the per-call branch to chooseLinearBinarySearchoverLinearScanwould itself cost the same. This matches FFF's existing pattern forBitInterpolationSearch: keep the strategy as opt-in for callers with workloads they've measured. Auto's per-query tree is unchanged.Test plan
MAXvalues, includingArgumentErroron bad valuesBase.searchsortedlast/firstfuzz on Float64 + Int64 acrossMAX ∈ {0, 1, 2, 4, 8, 16, 32, 64, 128}MAXvs gap =MAX + 1(binary fallback boundary)searchsortedfirstfinds the first occurrence after a backward walk)Please ignore until reviewed by @ChrisRackauckas.
🤖 Generated with Claude Code