Skip to content

Commit cb850f0

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 a specialized method for `Matrix` result with `AbstractVector` ydual that uses direct loop indexing instead of reshape+broadcast. This is zero-alloc under --check-bounds=yes and produces identical results. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff0d903 commit cb850f0

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/jacobian.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ function extract_jacobian!(::Type{T}, result::AbstractArray, ydual::AbstractArra
101101
return result
102102
end
103103

104+
# Avoid reshape allocation for the common case where result is already a correctly-shaped Matrix.
105+
# reshape() allocates a wrapper object that cannot be elided under --check-bounds=yes.
106+
function extract_jacobian!(::Type{T}, result::Matrix, ydual::AbstractVector, n) where {T}
107+
for j in 1:n
108+
for i in eachindex(ydual)
109+
result[i, j] = partials(T, ydual[i], j)
110+
end
111+
end
112+
return result
113+
end
114+
104115
function extract_jacobian!(::Type{T}, result::MutableDiffResult, ydual::AbstractArray, n) where {T}
105116
extract_jacobian!(T, DiffResults.jacobian(result), ydual, n)
106117
return result

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)