Depends on how the Jacobian structure is exploited. When both using brute force(or in general, exploit the jacobian’s structure to a same degree) the theory says AD should be faster. But when the matrix structure is put consideration brute force AD may not be. Demo below.
Finite difference with banded jacobian.
module func
use fazang
implicit none
contains
function f(x, xl, xr) result(fx)
real(rk), intent(in) :: x, xl, xr
real(rk) :: fx
fx = x*x + xl*xl + xr*xr
end function f
function jac(x, n) result(a)
integer, intent(in) :: n
real(rk), intent(in) :: x(n)
real(rk) :: a(n, n)
integer :: i, j
real(rk), parameter :: h = 0.01d0
a = 0.d0
! neglect the boundaries.
do i = 2, n-1
a(i, i-1) = (f(x(i), x(i-1)+h, x(i+1)) - f(x(i), x(i-1)-h, x(i+1)))/(2*h)
a(i, i) = (f(x(i)+h, x(i-1), x(i+1)) - f(x(i)-h, x(i-1), x(i+1)))/(2*h)
a(i, i+1) = (f(x(i), x(i-1), x(i+1)+h) - f(x(i), x(i-1), x(i+1)+h))/(2*h)
end do
end function jac
end module func
program jac_test
use fazang
use func
implicit none
integer, parameter :: n = 1000
integer i
real(rk) :: fx(n, n), x(n)
do i = 1, n
x(i) = 1.5d0 * i
end do
fx = jac(x, n)
end program jac_test
which takes
1/1 jacobian_fd OK 0.01s
Now brute force AD using the library I’m working on.
module func
use fazang
implicit none
contains
function f(x, n) result(fx)
integer, intent(in) :: n
type(var), intent(in) :: x(:)
type(var) :: fx(n)
integer :: i
fx(1) = x(1)*x(1) + x(2)*x(2)
fx(n) = x(n)*x(n) - x(n-1)*x(n-1)
do i = 2, n-1
fx(i) = x(i)*x(i) + x(i-1)*x(i-1) + x(i+1)*x(i+1)
end do
end function f
end module func
program jac_test
use fazang
use func
implicit none
integer, parameter :: n = 1000
integer i
real(rk) :: fx(n, n+1), x(n)
do i = 1, n
x(i) = 1.5d0 * i
end do
fx = jacobian(f, n, x)
end program jac_test
which takes
1/1 jacobian_ad OK 3.31s
The cause of performance gap is that in AD the jacobian is taken w.r.t. every x entry, without considering the sparsity.