Skip to content

Commit 012b92c

Browse files
authored
Support 32-bit platforms (#95)
The SIMD llvmcall kernels assume a 64-bit host: length(v)::Int32 fails to dispatch to the len::Int64 primitives, breaking precompilation with a MethodError, and on Julia < 1.12 Ptr arguments lower to i32, mismatching the declared i64. Gate the SIMD path behind Sys.WORD_SIZE == 64 and bind the primitives to a portable scalar loop with the identical contract elsewhere. Add a 32-bit CI job (Core group) so regressions are caught. Created by generative AI.
1 parent 3a65b85 commit 012b92c

4 files changed

Lines changed: 152 additions & 91 deletions

File tree

.github/workflows/Tests.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,12 @@ jobs:
2222
tests:
2323
uses: "SciML/.github/.github/workflows/grouped-tests.yml@v1"
2424
secrets: "inherit"
25+
tests-32bit:
26+
name: "Core (julia 1, ubuntu-latest, x86)"
27+
uses: "SciML/.github/.github/workflows/tests.yml@v1"
28+
with:
29+
julia-version: "1"
30+
julia-arch: "x86"
31+
group: "Core"
32+
coverage: false
33+
secrets: "inherit"

src/equality.jl

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ This function does **not** assume `A` is sorted. For sorted vectors, see
1414
`DenseVector{Int64}`) or [`findequal`](@ref) (the strategy-framework
1515
equality wrapper that returns an `Int` with a sentinel).
1616
17-
The `(x::Int64, A::DenseVector{Int64})` method uses a custom LLVM IR SIMD
18-
scan (load 8 lanes, `icmp eq`, `cttz` on the mask) — about 8× faster than
19-
the scalar `findfirst(==(x), v)` on modern x86-64. Every other element-type
17+
On 64-bit platforms the `(x::Int64, A::DenseVector{Int64})` method uses a
18+
custom LLVM IR SIMD scan (load 8 lanes, `icmp eq`, `cttz` on the mask) —
19+
about 8× faster than the scalar `findfirst(==(x), v)` on modern x86-64;
20+
on 32-bit platforms it uses a scalar loop. Every other element-type
2021
and array-storage combination falls back to `findfirst(isequal(x), A)`.
2122
"""
2223
findfirstequal(vpivot, ivars) = findfirst(isequal(vpivot), ivars)
2324
function findfirstequal(vpivot::Int64, ivars::DenseVector{Int64})
2425
GC.@preserve ivars begin
25-
ret = _findfirstequal(vpivot, pointer(ivars), length(ivars))
26+
ret = _findfirstequal(vpivot, pointer(ivars), Int64(length(ivars)))
2627
end
2728
return ret < 0 ? nothing : ret + 1
2829
end
@@ -67,7 +68,7 @@ function findfirstsortedequal(
6768
end
6869
# maybe occurs in vars[offset+1:offset+len]
6970
GC.@preserve vars begin
70-
ret = _findfirstequal(var, pointer(vars) + 8offset, len)
71+
ret = _findfirstequal(var, pointer(vars) + 8offset, Int64(len))
7172
end
7273
return ret < 0 ? nothing : ret + offset + 1
7374
end

src/simd_ir.jl

Lines changed: 113 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -82,93 +82,120 @@ function _simd_scan_ir(t, pred)
8282
"""
8383
end
8484

85-
const FFE_IR = _simd_scan_ir("i64", "eq")
86-
87-
function _findfirstequal(vpivot::Int64, ptr::Ptr{Int64}, len::Int64)
88-
return Base.llvmcall(
89-
(FFE_IR, "entry"),
90-
Int64,
91-
Tuple{Int64, Ptr{Int64}, Int64},
92-
vpivot,
93-
ptr,
94-
len
95-
)
85+
# Portable scalar implementation of the primitives' contract: the 0-based
86+
# offset of the first element with `pred(v[i], x)`, or -1 if there is none.
87+
# Backs the primitives on non-64-bit platforms, where the SIMD kernels
88+
# (64-bit lengths and pointer lowering) are unavailable.
89+
function _scalar_first(pred::P, x::T, ptr::Ptr{T}, len::Int64) where {P, T}
90+
for i in 1:len
91+
pred(unsafe_load(ptr, i), x) && return i - 1
92+
end
93+
return Int64(-1)
9694
end
9795

98-
const _SIMD_GT_I64_IR = _simd_scan_ir("i64", "sgt")
99-
const _SIMD_GE_I64_IR = _simd_scan_ir("i64", "sge")
100-
const _SIMD_GT_F64_IR = _simd_scan_ir("double", "ogt")
101-
const _SIMD_GE_F64_IR = _simd_scan_ir("double", "oge")
102-
103-
# Reverse-direction predicates: used by `SIMDLinearScan` under
104-
# `Base.Order.Reverse` ordering, where the array is decreasing and we want
105-
# to find the first lane where `v[i] < x` (searchsortedlast) or `v[i] <= x`
106-
# (searchsortedfirst).
107-
const _SIMD_LT_I64_IR = _simd_scan_ir("i64", "slt")
108-
const _SIMD_LE_I64_IR = _simd_scan_ir("i64", "sle")
109-
const _SIMD_LT_F64_IR = _simd_scan_ir("double", "olt")
110-
const _SIMD_LE_F64_IR = _simd_scan_ir("double", "ole")
111-
112-
# Backing primitives for SIMDLinearScan. Each returns the 0-based offset of
113-
# the first lane satisfying the predicate, or -1 if none. Caveat: NaN inputs
114-
# always compare false under the ordered `o*` float predicates, so NaN in `v`
115-
# or `x` produces "no match" rather than an exception — consistent with the
116-
# undefined-input contract for sorted Float64 vectors containing NaN.
117-
function _simd_first_gt(x::Int64, ptr::Ptr{Int64}, len::Int64)
118-
return Base.llvmcall(
119-
(_SIMD_GT_I64_IR, "entry"),
120-
Int64, Tuple{Int64, Ptr{Int64}, Int64},
121-
x, ptr, len
122-
)
123-
end
124-
function _simd_first_ge(x::Int64, ptr::Ptr{Int64}, len::Int64)
125-
return Base.llvmcall(
126-
(_SIMD_GE_I64_IR, "entry"),
127-
Int64, Tuple{Int64, Ptr{Int64}, Int64},
128-
x, ptr, len
129-
)
130-
end
131-
function _simd_first_gt(x::Float64, ptr::Ptr{Float64}, len::Int64)
132-
return Base.llvmcall(
133-
(_SIMD_GT_F64_IR, "entry"),
134-
Int64, Tuple{Float64, Ptr{Float64}, Int64},
135-
x, ptr, len
136-
)
137-
end
138-
function _simd_first_ge(x::Float64, ptr::Ptr{Float64}, len::Int64)
139-
return Base.llvmcall(
140-
(_SIMD_GE_F64_IR, "entry"),
141-
Int64, Tuple{Float64, Ptr{Float64}, Int64},
142-
x, ptr, len
143-
)
144-
end
96+
@static if Sys.WORD_SIZE == 64
97+
98+
const FFE_IR = _simd_scan_ir("i64", "eq")
99+
100+
function _findfirstequal(vpivot::Int64, ptr::Ptr{Int64}, len::Int64)
101+
return Base.llvmcall(
102+
(FFE_IR, "entry"),
103+
Int64,
104+
Tuple{Int64, Ptr{Int64}, Int64},
105+
vpivot,
106+
ptr,
107+
len
108+
)
109+
end
110+
111+
const _SIMD_GT_I64_IR = _simd_scan_ir("i64", "sgt")
112+
const _SIMD_GE_I64_IR = _simd_scan_ir("i64", "sge")
113+
const _SIMD_GT_F64_IR = _simd_scan_ir("double", "ogt")
114+
const _SIMD_GE_F64_IR = _simd_scan_ir("double", "oge")
115+
116+
# Reverse-direction predicates: used by `SIMDLinearScan` under
117+
# `Base.Order.Reverse` ordering, where the array is decreasing and we want
118+
# to find the first lane where `v[i] < x` (searchsortedlast) or `v[i] <= x`
119+
# (searchsortedfirst).
120+
const _SIMD_LT_I64_IR = _simd_scan_ir("i64", "slt")
121+
const _SIMD_LE_I64_IR = _simd_scan_ir("i64", "sle")
122+
const _SIMD_LT_F64_IR = _simd_scan_ir("double", "olt")
123+
const _SIMD_LE_F64_IR = _simd_scan_ir("double", "ole")
124+
125+
# Backing primitives for SIMDLinearScan. Each returns the 0-based offset of
126+
# the first lane satisfying the predicate, or -1 if none. Caveat: NaN inputs
127+
# always compare false under the ordered `o*` float predicates, so NaN in `v`
128+
# or `x` produces "no match" rather than an exception — consistent with the
129+
# undefined-input contract for sorted Float64 vectors containing NaN.
130+
function _simd_first_gt(x::Int64, ptr::Ptr{Int64}, len::Int64)
131+
return Base.llvmcall(
132+
(_SIMD_GT_I64_IR, "entry"),
133+
Int64, Tuple{Int64, Ptr{Int64}, Int64},
134+
x, ptr, len
135+
)
136+
end
137+
function _simd_first_ge(x::Int64, ptr::Ptr{Int64}, len::Int64)
138+
return Base.llvmcall(
139+
(_SIMD_GE_I64_IR, "entry"),
140+
Int64, Tuple{Int64, Ptr{Int64}, Int64},
141+
x, ptr, len
142+
)
143+
end
144+
function _simd_first_gt(x::Float64, ptr::Ptr{Float64}, len::Int64)
145+
return Base.llvmcall(
146+
(_SIMD_GT_F64_IR, "entry"),
147+
Int64, Tuple{Float64, Ptr{Float64}, Int64},
148+
x, ptr, len
149+
)
150+
end
151+
function _simd_first_ge(x::Float64, ptr::Ptr{Float64}, len::Int64)
152+
return Base.llvmcall(
153+
(_SIMD_GE_F64_IR, "entry"),
154+
Int64, Tuple{Float64, Ptr{Float64}, Int64},
155+
x, ptr, len
156+
)
157+
end
158+
159+
# Reverse-direction primitives.
160+
function _simd_first_lt(x::Int64, ptr::Ptr{Int64}, len::Int64)
161+
return Base.llvmcall(
162+
(_SIMD_LT_I64_IR, "entry"),
163+
Int64, Tuple{Int64, Ptr{Int64}, Int64},
164+
x, ptr, len
165+
)
166+
end
167+
function _simd_first_le(x::Int64, ptr::Ptr{Int64}, len::Int64)
168+
return Base.llvmcall(
169+
(_SIMD_LE_I64_IR, "entry"),
170+
Int64, Tuple{Int64, Ptr{Int64}, Int64},
171+
x, ptr, len
172+
)
173+
end
174+
function _simd_first_lt(x::Float64, ptr::Ptr{Float64}, len::Int64)
175+
return Base.llvmcall(
176+
(_SIMD_LT_F64_IR, "entry"),
177+
Int64, Tuple{Float64, Ptr{Float64}, Int64},
178+
x, ptr, len
179+
)
180+
end
181+
function _simd_first_le(x::Float64, ptr::Ptr{Float64}, len::Int64)
182+
return Base.llvmcall(
183+
(_SIMD_LE_F64_IR, "entry"),
184+
Int64, Tuple{Float64, Ptr{Float64}, Int64},
185+
x, ptr, len
186+
)
187+
end
188+
189+
else
190+
191+
_findfirstequal(vpivot::Int64, ptr::Ptr{Int64}, len::Int64) = _scalar_first(==, vpivot, ptr, len)
192+
_simd_first_gt(x::Int64, ptr::Ptr{Int64}, len::Int64) = _scalar_first(>, x, ptr, len)
193+
_simd_first_ge(x::Int64, ptr::Ptr{Int64}, len::Int64) = _scalar_first(>=, x, ptr, len)
194+
_simd_first_lt(x::Int64, ptr::Ptr{Int64}, len::Int64) = _scalar_first(<, x, ptr, len)
195+
_simd_first_le(x::Int64, ptr::Ptr{Int64}, len::Int64) = _scalar_first(<=, x, ptr, len)
196+
_simd_first_gt(x::Float64, ptr::Ptr{Float64}, len::Int64) = _scalar_first(>, x, ptr, len)
197+
_simd_first_ge(x::Float64, ptr::Ptr{Float64}, len::Int64) = _scalar_first(>=, x, ptr, len)
198+
_simd_first_lt(x::Float64, ptr::Ptr{Float64}, len::Int64) = _scalar_first(<, x, ptr, len)
199+
_simd_first_le(x::Float64, ptr::Ptr{Float64}, len::Int64) = _scalar_first(<=, x, ptr, len)
145200

146-
# Reverse-direction primitives.
147-
function _simd_first_lt(x::Int64, ptr::Ptr{Int64}, len::Int64)
148-
return Base.llvmcall(
149-
(_SIMD_LT_I64_IR, "entry"),
150-
Int64, Tuple{Int64, Ptr{Int64}, Int64},
151-
x, ptr, len
152-
)
153-
end
154-
function _simd_first_le(x::Int64, ptr::Ptr{Int64}, len::Int64)
155-
return Base.llvmcall(
156-
(_SIMD_LE_I64_IR, "entry"),
157-
Int64, Tuple{Int64, Ptr{Int64}, Int64},
158-
x, ptr, len
159-
)
160-
end
161-
function _simd_first_lt(x::Float64, ptr::Ptr{Float64}, len::Int64)
162-
return Base.llvmcall(
163-
(_SIMD_LT_F64_IR, "entry"),
164-
Int64, Tuple{Float64, Ptr{Float64}, Int64},
165-
x, ptr, len
166-
)
167-
end
168-
function _simd_first_le(x::Float64, ptr::Ptr{Float64}, len::Int64)
169-
return Base.llvmcall(
170-
(_SIMD_LE_F64_IR, "entry"),
171-
Int64, Tuple{Float64, Ptr{Float64}, Int64},
172-
x, ptr, len
173-
)
174201
end

test/core_tests.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,3 +1353,27 @@ end
13531353
end
13541354
end
13551355
end
1356+
1357+
@safetestset "Scalar primitive parity" begin
1358+
using FindFirstFunctions, StableRNGs
1359+
F = FindFirstFunctions
1360+
1361+
# `_scalar_first` backs the SIMD primitives on non-64-bit platforms;
1362+
# check it against `findfirst` on all platforms.
1363+
rng = StableRNG(2028)
1364+
for _ in 1:2_000
1365+
n = rand(rng, 0:64)
1366+
vi = rand(rng, Int64(-50):Int64(50), n)
1367+
xi = rand(rng, Int64(-60):Int64(60))
1368+
vf = randn(rng, n)
1369+
xf = randn(rng)
1370+
for (pred, v, x) in (
1371+
(==, vi, xi), (>, vi, xi), (>=, vi, xi), (<, vi, xi), (<=, vi, xi),
1372+
(>, vf, xf), (>=, vf, xf), (<, vf, xf), (<=, vf, xf),
1373+
)
1374+
ref = findfirst(e -> pred(e, x), v)
1375+
got = GC.@preserve v F._scalar_first(pred, x, pointer(v), Int64(length(v)))
1376+
@test got == (ref === nothing ? -1 : ref - 1)
1377+
end
1378+
end
1379+
end

0 commit comments

Comments
 (0)