From 63b1de63d32688133e550db9b201101128f3a842 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 8 Oct 2026 07:30:12 -0400 Subject: [PATCH 1/2] Add fast path to stack(::AbstractVectorOfArray) for numeric inner arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stack(VA) called stack(stack.(VA.u); dims): the inner stack.() applies stack to every element of VA.u before the outer stack sees them. That step exists to collapse nested AbstractVectorOfArray (or other non-plain-array) elements into plain arrays first, but when VA.u's elements are already plain numeric arrays, stack.() is a redundant elementwise copy — one extra allocation per inner array for no change in the result. Skip it when eltype(VA.u) <: AbstractArray{<:Number} and stack VA.u directly; fall back to the original stack(stack.(VA.u); dims) path for everything else (nested AbstractVectorOfArray elements, etc.), so that case is unaffected. n=1000 inner arrays: 28.4us/2006 allocs -> 5.6us/3 allocs (1.12), 52.2us/1002 allocs (1.10, unmeasured after since the audit reported "same on 1.10" was not re-verified for this specific candidate; the allocation drop alone (104160 -> 16088 bytes) is confirmed on 1.10). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Agent-Harness: Claude Code 2.1.294 Agent-Model: claude-sonnet-5 Agent-Session: https://claude.ai/code/session_01LPHREnnonfLg1VcE1EJovv --- src/vector_of_array.jl | 7 +++++++ test/Core/interface_tests.jl | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/vector_of_array.jl b/src/vector_of_array.jl index af4da6e9..6b4bb60a 100644 --- a/src/vector_of_array.jl +++ b/src/vector_of_array.jl @@ -1045,6 +1045,13 @@ function Base.append!( end function Base.stack(VA::AbstractVectorOfArray; dims = :) + # `stack.(VA.u)` exists to collapse any nested `AbstractVectorOfArray` + # (or other non-numeric-array) elements of `VA.u` into plain arrays before + # the outer `stack`. When the elements are already plain numeric arrays, + # that inner `stack.()` call is a redundant elementwise copy. + if eltype(VA.u) <: AbstractArray{<:Number} + return stack(VA.u; dims) + end return stack(stack.(VA.u); dims) end diff --git a/test/Core/interface_tests.jl b/test/Core/interface_tests.jl index c4aa6ae8..a5df1452 100644 --- a/test/Core/interface_tests.jl +++ b/test/Core/interface_tests.jl @@ -208,6 +208,14 @@ testva = VA[VA[ones(2, 2), 2ones(2, 2)], 3ones(2, 2, 2)] @test stack(testva) == [1.0 1.0; 1.0 1.0;;; 2.0 2.0; 2.0 2.0;;;; 3.0 3.0; 3.0 3.0;;; 3.0 3.0; 3.0 3.0] +# stack on plain-numeric inner arrays skips `stack.(VA.u)` (an elementwise +# copy of every inner array before the outer stack): only the final output +# array is allocated, instead of also allocating one copy per inner array. +numeric_stack = VectorOfArray([rand(2) for _ in 1:1000]) +stack(numeric_stack) # compile away type instability before measuring +@test (@allocated stack(numeric_stack)) < 20_000 # final 2x1000 Float64 array alone is 16000 bytes +@test stack(numeric_stack) == reduce(hcat, numeric_stack.u) + # convert array from VectorOfArray/DiffEqArray t = 1:8 recs = [rand(10, 7) for i in 1:8] From 8eceb31ba860d19e71a97927cac6354dff14a58b Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Fri, 16 Oct 2026 10:04:40 -0400 Subject: [PATCH 2/2] Restrict stack fast path to inputs where it matches the general path Skip the per-element `stack` only for dense `Array`s of at least one dimension with concrete numeric eltype and no empty elements. Elsewhere it is not a no-op: it turns BitArrays into Array{Bool}, narrows abstract eltypes, and throws on 0-d and empty arrays. Test that typeof and values match `stack(stack.(u))` for each kind of input, and that 0-d and empty inputs still throw. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 5.5 Agent-Harness: Claude Code 2.1.301 Agent-Model: claude-opus-5-5 Agent-Session: local session ID 100f0163-d124-45d3-953d-b9b815281c58 (headless claude -p, no URL) --- src/vector_of_array.jl | 11 ++++++----- test/Core/interface_tests.jl | 14 +++++++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/vector_of_array.jl b/src/vector_of_array.jl index 2b93ab99..4000f3db 100644 --- a/src/vector_of_array.jl +++ b/src/vector_of_array.jl @@ -1045,11 +1045,12 @@ function Base.append!( end function Base.stack(VA::AbstractVectorOfArray; dims = :) - # `stack.(VA.u)` exists to collapse any nested `AbstractVectorOfArray` - # (or other non-numeric-array) elements of `VA.u` into plain arrays before - # the outer `stack`. When the elements are already plain numeric arrays, - # that inner `stack.()` call is a redundant elementwise copy. - if eltype(VA.u) <: AbstractArray{<:Number} + # Skipping the per-element `stack` is only valid where it returns an identical + # copy: it turns `BitArray`s into `Array{Bool}`, narrows abstract eltypes, + # and throws on 0-d and empty arrays. + ET = eltype(VA.u) + if ET <: Array{<:Number} && isconcretetype(ET) && isconcretetype(eltype(ET)) && + ndims(ET) >= 1 && !any(isempty, VA.u) return stack(VA.u; dims) end return stack(stack.(VA.u); dims) diff --git a/test/Core/interface_tests.jl b/test/Core/interface_tests.jl index 88d72831..ebdffe58 100644 --- a/test/Core/interface_tests.jl +++ b/test/Core/interface_tests.jl @@ -208,14 +208,22 @@ testva = VA[VA[ones(2, 2), 2ones(2, 2)], 3ones(2, 2, 2)] @test stack(testva) == [1.0 1.0; 1.0 1.0;;; 2.0 2.0; 2.0 2.0;;;; 3.0 3.0; 3.0 3.0;;; 3.0 3.0; 3.0 3.0] -# stack on plain-numeric inner arrays skips `stack.(VA.u)` (an elementwise -# copy of every inner array before the outer stack): only the final output -# array is allocated, instead of also allocating one copy per inner array. numeric_stack = VectorOfArray([rand(2) for _ in 1:1000]) stack(numeric_stack) # compile away type instability before measuring @test (@allocated stack(numeric_stack)) < 20_000 # final 2x1000 Float64 array alone is 16000 bytes @test stack(numeric_stack) == reduce(hcat, numeric_stack.u) +stack_inputs = ( + [rand(3) for _ in 1:2], [rand(2, 2) for _ in 1:3], [[1, 2], [3, 4]], + Vector{Real}[[1, 2.0], [3, 4.0]], [trues(3), falses(3)], [trues(2, 2), falses(2, 2)], +) +for u in stack_inputs, dims in (:, 1, 2) + @test typeof(stack(VectorOfArray(u); dims)) == typeof(stack(stack.(u); dims)) + @test stack(VectorOfArray(u); dims) == stack(stack.(u); dims) +end +@test_throws BoundsError stack(VectorOfArray([fill(1.0), fill(2.0)])) +@test_throws ArgumentError stack(VectorOfArray([Float64[], Float64[]])) + # convert array from VectorOfArray/DiffEqArray t = 1:8 recs = [rand(10, 7) for i in 1:8]