You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Anonymous function creation is slow, and almost always necessary #873
Suppose I have a function f(x::Vector, p::SomeData), which I want to compute the Jacobian of. A common way to do this is
p =generate_some_data()
jac =ForwardDiff(x ->f(x, p), x0)
which has very bad performance on every call because we are instantiating a new anonymous function every time, which triggers compilation. A more clever way of doing this is
partially_applied_f = x ->f(x, p)
#repeat the lines below as much as needed
p =generate_some_data()
jac =ForwardDiff(partially_applied_f, x0)
which introduces a weird invisible dependency of the calls of partially_applied_f on the current state of p. Would it be feasible to update method signatures in this package to accept parameters at the call site? Such that the following code is valid:
p =generate_some_data()
jac =ForwardDiff(f, x0, parameter = p)
DifferentialEquations already do something similar with their method calls. This would remove a lot of overhead and imprevisibility in my use cases.
which makes a lot of sense, since every time we write x -> f(x, 2). we are actually creating a new anonymous function. The same behavior is found if we re-execute the line defining partially_applied_f, as can be seen by the change in the number in the output (usually something like #ANONYMOUS_FUNCTION_ID (generic function with 1 method)).
Yes, but only if you are doing it in the REPL and retyping it like that. If you have it in a function or store the closure to a variable it is only once.
which effectively shows that g was defined once and did not trigger compilation on the second call. However, this dependency on the variable current_p is quite weird and hard to manage. I do not see a way of avoiding both issues at once: the repeated compilation and the clear flow of data to partially applied functions.
A very similar discussion can be found in an old DifferentialEquations issue. I think the solution they arrived at would also be the best fit for ForwardDiff.
Inside a function like this there is only one x -> f(x, p), and no cost:
julia> using ForwardDiff, BenchmarkTools
julia> f(x, p) = x^p
f (generic function with 1 method)
julia> function mycalc(xx::Real)
p = 3
d = ForwardDiff.derivative(x -> f(x, p), xx)
d^2
end
mycalc (generic function with 1 method)
julia> @btime mycalc(1.0)
0.916 ns (0 allocations: 0 bytes)
9.0
In your example, g is a non-const global which depends on global current_p, this is bad for other reasons.
I posted the entire script, written in a file, with no let anywhere.
So, if I understood correctly:
The syntax ForwardDiff.derivative(x -> f(x, p), x0) is bad in the global context, because compilation is triggered everytime.
In the global/script context, you should define a closure before the calls to ForwardDiff. However, defining a function with the syntax g(x) = f(x, p) does not seem to trigger compilation upon redefinition.
The syntax ForwardDiff.derivative(x -> f(x, p), x0) is fine inside functions.
In your example, g is a non-const global which depends on global current_p, this is bad for other reasons.
I understand this is bad, yes, it feels very messy. In your example, compilation must have happened at some point, no? I don't think it is possible for Julia to not compile code.
The behavior of this is not super clear to me. I've had issues with nested anonymous functions passed around many function calls basically triggering compilation at every call to ForwardDiff. So, I guess allowing for extra parameters does not make sense here. Maybe explain this somewhere? I get that it is a direct consequence of Julia's compilation rules but it is not at all obvious.
It can confusing to get used to, but none of this is all that specific to ForwardDiff. Performance-sensitive code should never just be sitting at top-level of a script or at the REPL, it should always be inside a function.
Yes, everything gets compiled. It's fine to leave things you only call once at top level -- you are implicitly stating that you don't care to save their compiled version for re-use.
Activity
KristofferC commentedon Nov 6, 2026
What do you mean here with "every time"? Only once, right?
pekpuglia commentedon Nov 6, 2026
I ran the experiment:
which makes a lot of sense, since every time we write
x -> f(x, 2). we are actually creating a new anonymous function. The same behavior is found if we re-execute the line definingpartially_applied_f, as can be seen by the change in the number in the output (usually something like #ANONYMOUS_FUNCTION_ID (generic function with 1 method)).KristofferC commentedon Nov 6, 2026
Yes, but only if you are doing it in the REPL and retyping it like that. If you have it in a function or store the closure to a variable it is only once.
pekpuglia commentedon Nov 6, 2026
Well, yes, but then you get this weird behavior where the value of your derivative depends on some current value of the variable that was captured:
outputs
which effectively shows that g was defined once and did not trigger compilation on the second call. However, this dependency on the variable current_p is quite weird and hard to manage. I do not see a way of avoiding both issues at once: the repeated compilation and the clear flow of data to partially applied functions.
pekpuglia commentedon Nov 6, 2026
A very similar discussion can be found in an old DifferentialEquations issue. I think the solution they arrived at would also be the best fit for ForwardDiff.
KristofferC commentedon Nov 6, 2026
You use a
letblock for that, right?mcabbott commentedon Nov 6, 2026
Inside a function like this there is only one
x -> f(x, p), and no cost:In your example,
gis a non-const global which depends on globalcurrent_p, this is bad for other reasons.pekpuglia commentedon Nov 6, 2026
I posted the entire script, written in a file, with no
letanywhere.So, if I understood correctly:
ForwardDiff.derivative(x -> f(x, p), x0)is bad in the global context, because compilation is triggered everytime.g(x) = f(x, p)does not seem to trigger compilation upon redefinition.ForwardDiff.derivative(x -> f(x, p), x0)is fine inside functions.I understand this is bad, yes, it feels very messy. In your example, compilation must have happened at some point, no? I don't think it is possible for Julia to not compile code.
The behavior of this is not super clear to me. I've had issues with nested anonymous functions passed around many function calls basically triggering compilation at every call to ForwardDiff. So, I guess allowing for extra parameters does not make sense here. Maybe explain this somewhere? I get that it is a direct consequence of Julia's compilation rules but it is not at all obvious.
mcabbott commentedon Nov 6, 2026
It can confusing to get used to, but none of this is all that specific to ForwardDiff. Performance-sensitive code should never just be sitting at top-level of a script or at the REPL, it should always be inside a function.
https://docs.julialang.org/en/v1/manual/performance-tips/#Performance-critical-code-should-be-inside-a-function
Yes, everything gets compiled. It's fine to leave things you only call once at top level -- you are implicitly stating that you don't care to save their compiled version for re-use.