From bba033cb7fc32777cd3df95b7605eccff1675504 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Thu, 23 Jul 2026 19:11:03 -0400 Subject: [PATCH] Same-shape fast path for restructure(x::Array, y::Array) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `restructure(x::Array, y)` is `reshape(convert(Array, y), size(x)...)`. `reshape` has no same-shape short-circuit (unlike `vec`), so reshaping an array to its own shape still mints a fresh `Array` header (sharing the data) — a per-call heap allocation. Return `y` directly when it already has `x`'s shape. Semantics unchanged: the `::Array` path already returned a data-sharing reshape, so `=== y` preserves the aliasing contract, and a genuine shape change still reshapes. On Julia 1.12 this removes a 32-byte allocation per call; in NonlinearSolve's Newton descent (which restructures an already-shaped `δu` buffer once per iteration) it cuts ~56% of a NonlinearSolveAlg solve's total allocation. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NB3oFTNzGW79UtDt8AyjsM --- Project.toml | 2 +- src/ArrayInterface.jl | 8 ++++++++ test/core.jl | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index c2d27182..6c6ab1fe 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ArrayInterface" uuid = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" -version = "7.28.0" +version = "7.28.1" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" diff --git a/src/ArrayInterface.jl b/src/ArrayInterface.jl index 0ecfec03..08600078 100644 --- a/src/ArrayInterface.jl +++ b/src/ArrayInterface.jl @@ -674,6 +674,14 @@ function restructure(x::Array, y) reshape(convert(Array, y), Base.size(x)...) end +function restructure(x::Array, y::Array) + # When `y` already has `x`'s shape it is its own restructuring. `reshape` is not a no-op + # here: unlike `vec`, it has no same-shape short-circuit and always mints a fresh `Array` + # header (sharing the data), so returning `y` avoids that per-call allocation. + Base.size(x) == Base.size(y) && return y + reshape(convert(Array, y), Base.size(x)...) +end + abstract type AbstractDevice end abstract type AbstractCPU <: AbstractDevice end struct CPUPointer <: AbstractCPU end diff --git a/test/core.jl b/test/core.jl index 3ad091b5..8676f059 100644 --- a/test/core.jl +++ b/test/core.jl @@ -153,6 +153,32 @@ end @test size(yr) == () @test yr == y end + + @testset "same-shape Array fast path" begin + # When `y` already has `x`'s shape, `restructure` returns `y` itself. `reshape` has no + # same-shape short-circuit (unlike `vec`), so it would otherwise mint a fresh Array + # header (sharing the data) on every call. + for dims in ((3,), (2,3), (2,2,2)) + x = rand(dims...) + y = rand(dims...) + @test ArrayInterface.restructure(x, y) === y + end + # ... and does so without allocating + function _restructure_alloc() + x = rand(4); y = rand(4) + ArrayInterface.restructure(x, y) + @allocated ArrayInterface.restructure(x, y) + end + @test _restructure_alloc() == 0 + + # a genuine shape change still reshapes to `x`'s shape (unchanged behavior) + x = rand(2,2) + y = rand(4) + yr = ArrayInterface.restructure(x, y) + @test yr isa Matrix{Float64} + @test size(yr) == (2,2) + @test vec(yr) == y + end end @testset "isstructured" begin