Skip to content

Commit d5b976e

Browse files
Split src/FindFirstFunctions.jl into a 10-file hierarchy
The package source had grown to a single 1900-line file mixing every layer: LLVM IR templates, equality-search primitives, strategy type definitions, the per-strategy dispatch methods, Auto's heuristic decision tree, the batched in-place API, the Guesser correlated-lookup helper, the findequal strategy-framework wrapper, and the PrecompileTools workload. Splitting into one file per logical layer makes the source navigable without changing any behaviour. src/ ├── FindFirstFunctions.jl 39 lines module + exports + includes ├── simd_ir.jl 135 IR template + IR constants + SIMD │ primitives (eq, sgt, sge, ogt, oge) ├── equality.jl 73 findfirstequal + findfirstsortedequal ├── strategies.jl 268 SearchStrategy abstract + concrete │ strategy types + SearchProperties + │ Auto (struct + docstrings) ├── search_properties.jl 120 _sampled_looks_linear / │ _sampled_looks_log_linear / _has_nan │ + populated SearchProperties ctor ├── dispatch.jl 583 Per-strategy searchsortedfirst/last │ methods + bracketstrictlymonotonic* │ + searchsortedfirstexp + _interp_guess │ + _bit_interp_guess_f64 + │ _simdscan_*_specialized ├── auto.jl 168 Auto crossover constants + per-query │ Auto + _estimate_avg_gap + │ _auto_simd_eligible / │ _auto_simd_gap_max / │ _auto_interp_eligible + _auto_pick ├── batched.jl 321 searchsortedfirst!/last! + │ sorted/unsorted inner loops + │ _take_sorted_path + generic │ _batched! + Auto-specialized │ _batched! + searchsortedrange ├── guesser.jl 109 looks_linear + Guesser + GuesserHint │ dispatch (relies on BracketGallop) ├── findequal.jl 91 findequal generic + BisectThenSIMD │ shortcut for DenseVector{Int64} └── precompile.jl 58 PrecompileTools @setup_workload Include order is significant — each file may depend on names from earlier files. The order is documented in `FindFirstFunctions.jl` and is: simd_ir → equality → strategies → search_properties → dispatch → auto → batched → guesser → findequal → precompile. The two non-obvious dependencies are: - `auto.jl` references `_sampled_looks_linear` from `search_properties.jl` (the `_auto_interp_eligible` loose tier). - `batched.jl` references `_estimate_avg_gap`, `_auto_simd_eligible`, `_auto_simd_gap_max`, `_auto_interp_eligible`, `_AUTO_*` constants from `auto.jl` (the Auto-specialized `_batched!` lives in `batched.jl` because it shares the sorted/unsorted-loop infrastructure with the generic `_batched!`). 84626 tests pass; no behavioural change. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent aaaf5a5 commit d5b976e

11 files changed

Lines changed: 1941 additions & 1930 deletions

src/FindFirstFunctions.jl

Lines changed: 15 additions & 1930 deletions
Large diffs are not rendered by default.

src/auto.jl

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Auto strategy — heuristic dispatch tree + crossover constants + the
2+
# helpers (`_estimate_avg_gap`, `_auto_simd_*`, `_auto_interp_eligible`) it
3+
# consults. Per-query Auto dispatch lives here; batched Auto dispatch lives
4+
# in `batched.jl` (next to the generic `_batched!` it specializes).
5+
6+
# Per-query Auto threshold: under this length, the bracket-search bookkeeping
7+
# costs more than a worst-case linear walk.
8+
const _AUTO_LINEAR_THRESHOLD = 16
9+
10+
# Batched-Auto crossover: at gap ≤ 4 LinearScan beats ExpFromLeft (its 5
11+
# initial linear probes are wasted when the gap is already 0 or 1). Above 4,
12+
# the SIMD path picks up (where eligible) up to gap = `_auto_simd_gap_max`.
13+
const _AUTO_BATCH_LINEAR_GAP = 4
14+
15+
# For sparse queries (gap large) on long vectors, InterpolationSearch can
16+
# beat ExpFromLeft by ~2× on uniformly-spaced data. The sampled-linearity
17+
# check is O(1) — 9 probes — so it's cheap enough to run inside Auto when
18+
# there's a real chance of unlocking InterpolationSearch.
19+
const _AUTO_INTERP_MIN_GAP = 8
20+
const _AUTO_INTERP_MIN_N = 1024
21+
const _AUTO_INTERP_MIN_M = 2
22+
23+
# Very-sparse override: when the gap is large enough that ExpFromLeft's
24+
# log₂(gap) doubling levels approach InterpolationSearch's log₂(n) worst-case
25+
# binary refinement, InterpolationSearch's better cache behaviour (one
26+
# extrapolation jump + local refine vs. many doubling probes across the
27+
# array) wins even on non-strictly-linear data.
28+
const _AUTO_INTERP_LOOSE_GAP = 256
29+
const _AUTO_LINEAR_LOOSE_TOLERANCE = 5.0e-2
30+
31+
# SIMDLinearScan eligibility window. The threshold is eltype-parameterized
32+
# via the `_auto_simd_gap_max` function below.
33+
@inline _auto_simd_gap_max(::DenseVector{Int64}) = 64
34+
@inline _auto_simd_gap_max(::DenseVector{Float64}) = 64
35+
@inline _auto_simd_gap_max(::AbstractVector) = 0 # not SIMD-eligible
36+
37+
# When InterpolationSearch isn't eligible and the gap is large, BracketGallop
38+
# beats ExpFromLeft because the 5 linear probes ExpFromLeft does upfront are
39+
# wasted (no chance the answer is within 5 of `hint = prev_result` when the
40+
# gap is hundreds or thousands). BracketGallop just starts doubling
41+
# immediately. The bench sweep shows this crossover at gap ≈ 16.
42+
const _AUTO_GALLOP_GAP_MIN = 16
43+
44+
# Per-query Auto: pick based on hint validity and length(v).
45+
@inline function _auto_pick(v::AbstractVector, hint::Integer)
46+
return if hint < firstindex(v) || hint > lastindex(v)
47+
BinaryBracket()
48+
elseif length(v) <= _AUTO_LINEAR_THRESHOLD
49+
LinearScan()
50+
else
51+
BracketGallop()
52+
end
53+
end
54+
55+
# Returns `(gap, skewed)`: the estimated average step in `v`'s index space
56+
# between consecutive query results, plus a flag that's true when the
57+
# queries' distribution is non-uniform within their span.
58+
#
59+
# - `gap` is the per-query cost driver. We always use the span-based
60+
# estimate `n * span(queries) / span(v) / m` so that tightly-clustered
61+
# queries (span_q ≈ 0) report gap ≈ 0 regardless of `n/m`. The earlier
62+
# `n / m` fallback for skewed queries caused `SIMDLinearScan` to be
63+
# picked for clustered queries where LinearScan's tiny scalar walk is
64+
# 5× faster.
65+
# - `skewed` is an InterpolationSearch-suitability flag. When the median
66+
# query sits well off the midpoint of `queries[1]..queries[end]`, the
67+
# queries are clustered within their span and the per-call linear
68+
# extrapolation guess is worse than the previous-result hint that
69+
# `ExpFromLeft` would use.
70+
@inline function _estimate_avg_gap(
71+
v::AbstractVector{<:Number}, queries::AbstractVector{<:Number}, m::Integer,
72+
)
73+
n = length(v)
74+
n <= 1 && return (0, false)
75+
@inbounds span_v = v[end] - v[1]
76+
if iszero(span_v) || !isfinite(span_v)
77+
return (n ÷ max(1, m), false)
78+
end
79+
@inbounds span_q = queries[end] - queries[1]
80+
# Skew detection on small `m` is too noisy — for `m ≈ 4` random uniform
81+
# samples, the median routinely sits 30%+ off the linear midpoint by
82+
# chance. Gate on `m ≥ 10` where the statistical variance is well below
83+
# the 20% threshold.
84+
skewed = false
85+
if m >= 10
86+
@inbounds mid_q = queries[firstindex(queries) + m ÷ 2]
87+
@inbounds expected_mid = (
88+
queries[firstindex(queries)] +
89+
queries[lastindex(queries)]
90+
) / 2
91+
if !iszero(span_q) &&
92+
abs(mid_q - expected_mid) > 0.2 * abs(span_q)
93+
skewed = true
94+
end
95+
end
96+
ratio = span_q / span_v
97+
# Clamp ratio: queries may extend outside v's range (extrapolation).
98+
ratio = clamp(ratio, zero(ratio), one(ratio))
99+
return (floor(Int, n * ratio / max(1, m)), skewed)
100+
end
101+
102+
# Non-numeric eltypes: no span subtraction possible, fall back to length
103+
# ratio and assume queries are roughly uniform (no skew detection possible).
104+
@inline _estimate_avg_gap(
105+
v::AbstractVector, ::AbstractVector, m::Integer,
106+
) = (length(v) ÷ max(1, m), false)
107+
108+
# SIMD eligibility check used by Auto's batched dispatch. The static type
109+
# test on `v` discriminates the `DenseVector{Int64}` / `DenseVector{Float64}`
110+
# cases that SIMDLinearScan supports. For Float64, NaN presence is taken from
111+
# cached `SearchProperties.has_nan` when available; otherwise we assume no
112+
# NaN — Base's positional search doesn't check sortedness either, and the
113+
# burden of supplying populated props is on the caller for pathological
114+
# inputs.
115+
@inline _auto_simd_eligible(v::DenseVector{Int64}, ::SearchProperties) = true
116+
@inline function _auto_simd_eligible(v::DenseVector{Float64}, p::SearchProperties)
117+
return p.has_props ? !p.has_nan : true
118+
end
119+
@inline _auto_simd_eligible(::AbstractVector, ::SearchProperties) = false
120+
121+
# InterpolationSearch eligibility: two-tier linearity check. For
122+
# `_AUTO_INTERP_MIN_GAP ≤ gap < _AUTO_INTERP_LOOSE_GAP` we require strict
123+
# linearity (`_AUTO_LINEAR_REL_TOLERANCE`, default 0.1%) — InterpolationSearch
124+
# is only worth the per-call cost on truly uniform data when ExpFromLeft is
125+
# also competitive. For `gap ≥ _AUTO_INTERP_LOOSE_GAP` we accept a looser
126+
# tolerance (`_AUTO_LINEAR_LOOSE_TOLERANCE`, default 5%) because the cache
127+
# benefit of one extrapolation jump + local refine compensates for a worse
128+
# guess, but we still reject genuinely nonlinear data (log-spaced,
129+
# two-scale) where InterpolationSearch loses 2–3× to ExpFromLeft.
130+
@inline function _auto_interp_eligible(v, props::SearchProperties, gap::Integer)
131+
if gap >= _AUTO_INTERP_LOOSE_GAP
132+
# Loose probe — even on cached props, the strict `is_linear` bit may
133+
# already reflect a tighter threshold than we need here, so run the
134+
# sampled probe at the loose tolerance regardless of cache state.
135+
return _sampled_looks_linear(v, _AUTO_LINEAR_LOOSE_TOLERANCE)
136+
end
137+
return props.has_props ? props.is_linear : _sampled_looks_linear(v)
138+
end
139+
140+
# Per-query Auto dispatch.
141+
function Base.searchsortedlast(
142+
::Auto, v::AbstractVector, x, hint::Integer;
143+
order::Base.Order.Ordering = Base.Order.Forward,
144+
)
145+
s = _auto_pick(v, hint)
146+
return s isa BinaryBracket ?
147+
searchsortedlast(s, v, x; order = order) :
148+
searchsortedlast(s, v, x, hint; order = order)
149+
end
150+
151+
function Base.searchsortedfirst(
152+
::Auto, v::AbstractVector, x, hint::Integer;
153+
order::Base.Order.Ordering = Base.Order.Forward,
154+
)
155+
s = _auto_pick(v, hint)
156+
return s isa BinaryBracket ?
157+
searchsortedfirst(s, v, x; order = order) :
158+
searchsortedfirst(s, v, x, hint; order = order)
159+
end
160+
161+
Base.searchsortedlast(
162+
::Auto, v::AbstractVector, x;
163+
order::Base.Order.Ordering = Base.Order.Forward,
164+
) = searchsortedlast(BinaryBracket(), v, x; order = order)
165+
Base.searchsortedfirst(
166+
::Auto, v::AbstractVector, x;
167+
order::Base.Order.Ordering = Base.Order.Forward,
168+
) = searchsortedfirst(BinaryBracket(), v, x; order = order)

0 commit comments

Comments
 (0)