Add LinearBinarySearch{MAX}: bounded linear walk + binary fallback - #72
Draft
ChrisRackauckas-Claude wants to merge 2 commits into
Draft
ChrisRackauckas-Claude wants to merge 2 commits into
ChrisRackauckas-Claude wants to merge 2 commits 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>
… to v3 API
Upstream replaced the v2 Base.searchsortedlast(::S, ...) extensions with
the v3 StrategyKind enum + kernels architecture (src/dispatch.jl deleted,
split into src/kernels.jl + src/kinds.jl + src/strategy_kind.jl; tests
moved to test/core_tests.jl via SciMLTesting).
Port preserving intent:
- LinearBinarySearch{MAX} walks/kernels move verbatim to src/kernels.jl
as _kernel_last/first_linear_binary_search with trailing Val{MAX}.
- Struct entry points in src/strategy_kind.jl (multimethod, like
GuesserHint); strategy_kind(::LinearBinarySearch) throws ArgumentError
since no single enum tag can represent every MAX. Opt-in only: Auto
unchanged, no new StrategyKind value.
- LBS tests move to test/core_tests.jl with searchsorted_last/first
names; guard test covers LinearBinarySearch{8} for no-Base-extension.
- bench/linearbinary_sweep.jl updated to searchsorted_last.
Verified: Core 208501 pass, QA 21+34 pass, docs build clean,
Runic --check clean, typos clean.
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Agent-Harness: opencode 1.18.31
Agent-Model: opencode/muse-spark-1.3-contributor-free
Agent-Session: /home/crackauc/sandbox/fleet-master-jobs/nw-findfirstfunctions-72/log.txt
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
CI triage (2026-09-25)
The head commit (
9b82f92, May 2026) sat on a 4-month-stale base and everyfailing check's logs have since expired (GitHub API returns HTTP 410 for run
26253914507et al.), so classification below is by failure signature againstthe current base (
main@2263792, all green — e.g. Tests35739730551,IntegrationTest
35739730245, Documentation35739730216successes on2026-09-22). The branch has now been merged up to current
main(
58b3e9e, merge commit, no rebase) with the feature ported to the v3 API(see commit message), which supersedes the stale results.
GROUP=QApasses locally on merged head (21 + 34)typosclean locally; upstream_typos.tomlmerged in)26253914507Local verification on the merged head (
58b3e9e):GROUP=Core208501 pass,GROUP=QA21 + 34 pass, docsmake.jlbuilds clean (with the package devved,as CI does),
Runic --checkclean,typosclean. New CI on the merged headis running (runs
36088032xxx).Risk assessment
LinearBinarySearch, opt-in only).No existing dispatch changed: singleton enum/kind switches untouched,
Autopicker untouched, batched paths route LBS through the generic struct loop.
Files touched by the port:
src/kernels.jl(append-only LBS section),src/strategy_kind.jl(+4 entry points, +1strategy_kindthrow),src/strategies.jl(LBS struct, unchanged semantics),test/core_tests.jl(new testset),
bench/linearbinary_sweep.jl(v3 call names).src/dispatch.jldeletion is upstream's v3 redesign, not this PR.
GROUP=Core: 208501 pass;GROUP=QA: 55 pass; LBS testsetalone: 36296 pass; docs build clean; Runic/typos clean.
pattern in the v3 architecture (mirrors
GuesserHintprecedent); confirmthe maintainers want it vs. a different integration.
🤖 Posted by an AI agent — harness: opencode 1.18.31 · model: opencode/muse-spark-1.3-contributor-free
Conversation: /home/crackauc/sandbox/fleet-master-jobs/nw-findfirstfunctions-72/log.txt