Skip to content

Commit 526aecd

Browse files
Avoid reshape allocation in extract_jacobian! for Matrix results
`extract_jacobian!` called `reshape(result, length(ydual), n)` which allocates a 48-byte ReshapedArray wrapper. Under `--check-bounds=yes` (used by Pkg.test), this allocation cannot be elided by the compiler, causing 48 bytes per jacobian! call. For implicit ODE/SDE solvers that call jacobian! multiple times per step, this adds up (e.g. 144 bytes/step for SKenCarp with 3 NL solver iterations). Add `_maybe_reshape` that returns the array as-is when it already has the target shape, avoiding the wrapper allocation. Falls back to `reshape` when dimensions don't match. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff0d903 commit 526aecd

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

src/jacobian.jl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jacobian(f, x::Real) = throw(DimensionMismatch("jacobian(f, x) expects that x is
9393
#####################
9494

9595
function extract_jacobian!(::Type{T}, result::AbstractArray, ydual::AbstractArray, n) where {T}
96-
out_reshaped = reshape(result, length(ydual), n)
96+
out_reshaped = _maybe_reshape(result, length(ydual), n)
9797
ydual_reshaped = vec(ydual)
9898
# Use closure to avoid GPU broadcasting with Type
9999
partials_wrap(ydual, nrange) = partials(T, ydual, nrange)
@@ -117,6 +117,16 @@ function extract_jacobian_chunk!(::Type{T}, result, ydual, index, chunksize) whe
117117
return result
118118
end
119119

120+
# Avoid allocating a ReshapedArray wrapper when `result` already has the target shape.
121+
# reshape() always allocates a wrapper that cannot be elided under --check-bounds=yes.
122+
@inline function _maybe_reshape(result::AbstractArray, m, n)
123+
if size(result) == (m, n)
124+
return result
125+
else
126+
return reshape(result, m, n)
127+
end
128+
end
129+
120130
reshape_jacobian(result, ydual, xdual) = reshape(result, length(ydual), length(xdual))
121131
reshape_jacobian(result::DiffResult, ydual, xdual) = reshape_jacobian(DiffResults.jacobian(result), ydual, xdual)
122132

test/AllocationsTest.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,29 @@ convert_test_574() = convert(ForwardDiff.Dual{Nothing,ForwardDiff.Dual{Nothing,F
2828
@test iszero(allocs_convert_test_574())
2929
end
3030

31+
@testset "Test extract_jacobian! allocations" begin
32+
# extract_jacobian! should not allocate when result is a Matrix.
33+
# Previously, reshape() inside extract_jacobian! allocated a wrapper
34+
# object that could not be elided under --check-bounds=yes.
35+
T = ForwardDiff.Tag{Nothing, Float64}
36+
N = 3
37+
ydual = ForwardDiff.Dual{T}.(
38+
[1.0, 2.0, 3.0],
39+
[ForwardDiff.Partials((1.0, 2.0, 3.0)),
40+
ForwardDiff.Partials((4.0, 5.0, 6.0)),
41+
ForwardDiff.Partials((7.0, 8.0, 9.0))]
42+
)
43+
result = zeros(3, 3)
44+
45+
allocs_extract!() = @allocated ForwardDiff.extract_jacobian!(T, result, ydual, N)
46+
allocs_extract!() # warmup
47+
@test iszero(allocs_extract!())
48+
49+
# Verify correctness: result[i,j] should be ∂yᵢ/∂xⱼ
50+
ForwardDiff.extract_jacobian!(T, result, ydual, N)
51+
@test result == [1.0 2.0 3.0;
52+
4.0 5.0 6.0;
53+
7.0 8.0 9.0]
54+
end
55+
3156
end

0 commit comments

Comments
 (0)