julia
201e056e - perf: bulk fast paths for NTuple-from-array and rand!(::Array{Complex}) (#61762)

Commit
140 days ago
perf: bulk fast paths for NTuple-from-array and rand!(::Array{Complex}) (#61762) I pointed Claude at [perf.julialang.org](https://perf.julialang.org/) and asked it to find opportunities to improve unusually slow benchmarks. See [JuliaCI/julia-ci-timing@main/analysis](https://github.com/JuliaCI/julia-ci-timing/tree/main/analysis) for some of the scripts it used. This is what it came up with: ---- Two small, BaseBenchmarks-driven perf fixes that share a theme — when the storage of an array already has the layout you want, dispatch to the bulk path on a `reinterpret` view instead of going through the per-element fallback. ### `NTuple{N,E}(::Union{Array,Memory})` for `N >= 32` The default `_totuple(::Type{All32{E,N}}, itr)` path collects into an intermediate `Vector` and splats. The `All32` cap exists so we don't specialize tuple construction on every `N`; this PR keeps that property (no per-`N` codegen) by adding a single fast path for `Array`/`Memory` inputs whose `isbits` element type matches the requested tuple element type. In that case the storage is layout-identical to the tuple, so a single `reinterpret` view + scalar load returns the tuple in O(1) (plus a length check). The fast path lives in `reinterpretarray.jl` because `reinterpret` on arrays isn't yet defined when `tuple.jl` is loaded. Over-long inputs are silently truncated to match the iterator-based fallback. Non-matching eltypes / non-isbits eltypes fall through to the existing path unchanged. ``` master this PR NTuple{40,Float64}(::Vector{Float64}) 1.36 μs 7.3 ns NTuple{150,Float64}(::Vector{Float64}) 4.92 μs 17.0 ns ``` (0 allocations on the fast path, vs. `N+~3` allocations and `~33 N` B on master.) ### Bulk `rand!(::Array{Complex{T}})` for `T <: HWReal` `rand!(::AbstractRNG, ::Array{Complex{T}})` previously fell through to the generic `AbstractArray` path — two scalar `rand` calls per element. A packed `Array{Complex{T}}` has the same memory layout as a length-`2N` `Array{T}`, so reinterpreting and dispatching to the bulk `rand!` for `T` (SIMD-vectorized for `HWReal` on Xoshiro/TaskLocalRNG, and pointer- based for MersenneTwister) is correct and substantially faster. Per @vtjnash's feedback in earlier review, this avoids `unsafe_wrap`/`unsafe_load` entirely. Instead, the existing bulk dispatches in `MersenneTwister` and `XoshiroSimd` are widened to also accept a `NonReshapedReinterpretArray` over a `MutableDenseArrayType` parent. `pointer`/`unsafe_convert` are already defined for `ReinterpretArray` and forward to the parent's storage, so the existing `GC.@preserve A ... pointer(A) ...` machinery keeps working unchanged. Speedups on a length-1000 `Vector{Complex{T}}`, default RNG: ``` master this PR Complex{Float16} 3.5 μs 1.5 μs 2.4× Complex{Float32} 3.4 μs 2.2 μs 1.5× Complex{Float64} 3.1 μs 3.1 μs ≈ Complex{Int8} 3.1 μs 539 ns 5.7× Complex{Int16} 3.1 μs 1.1 μs 3.0× Complex{Int32} 3.1 μs 1.9 μs 1.7× Complex{Int64} 4.6 μs 4.2 μs 1.1× Complex{UInt8} 3.1 μs 532 ns 5.9× Complex{UInt16} 3.1 μs 1.0 μs 2.9× Complex{UInt32} 3.0 μs 1.9 μs 1.6× Complex{UInt64} 4.7 μs 4.2 μs 1.1× ``` All variants are 0-allocation. `Complex{Float64}` and the 64-bit integer variants were already close to the bulk path's throughput on master and see only a small win. A reproducibility test was added so equal MT seeds keep producing equal sequences for `Vector{Complex{T}}`. --- Disclosure: written with the assistance of generative AI (GitHub Copilot / Claude). --------- Co-authored-by: GitHub Copilot <noreply@github.com>
Parents
Loading