III.14 · Calculus
Why is a smaller step not always a better derivative?
Compute forward, backward and centered difference quotients in binary64 for steps from 10⁻¹ to 10⁻¹⁶. The error first falls with h, then rises again — two error sources trade places.
Shrink h and watch two errors compete
log₁₀|error| vs log₁₀ h: forward (blue), backward (dashed green), centered (orange), budgets (dashed purple)
Observed errors of three binary64 difference quotients of f(x) = sin x at x = 1 for h = 10⁻¹ … 10⁻¹⁶, against the reference derivative cos x. Errors first fall (truncation) then rise (rounding); the dashed curves are the documented error budgets. At h = 10⁻⁴: forward 4.21×10⁻⁵, centered 9.00×10⁻¹⁰.
- Point
- 1
- Step
- 10⁻⁴
- Reference derivative (cos x)
- 0.540302
- Forward difference
- 0.540260
- Backward difference
- 0.540344
- Centered difference
- 0.540302
- Forward error
- 4.21×10⁻⁵
- Centered error
- 9.00×10⁻¹⁰
- Forward truncation budget
- 5.00×10⁻⁵
- Forward rounding budget
- 3.80×10⁻¹²
- Machine epsilon (binary64)
- 2.22×10⁻¹⁶
Exact values use =; binary64 approximations use ≈. Undefined values state their reason.
At h = 10⁻⁴ truncation dominates: shrinking h still helps. The centered difference (error 9.00×10⁻¹⁰) beats forward (4.21×10⁻⁵) because its truncation error is O(h²).
| k | h | forward error | centered error | forward budget |
|---|---|---|---|---|
| 1 | 10⁻¹ | 4.29×10⁻² | 9.00×10⁻⁴ | 2.00×10⁻¹ |
| 2 | 10⁻² | 4.22×10⁻³ | 9.00×10⁻⁶ | 2.00×10⁻² |
| 3 | 10⁻³ | 4.21×10⁻⁴ | 9.01×10⁻⁸ | 2.00×10⁻³ |
| 4 | 10⁻⁴ | 4.21×10⁻⁵ | 9.00×10⁻¹⁰ | 2.00×10⁻⁴ |
| 5 | 10⁻⁵ | 4.21×10⁻⁶ | 1.11×10⁻¹¹ | 2.00×10⁻⁵ |
| 6 | 10⁻⁶ | 4.21×10⁻⁷ | 2.77×10⁻¹¹ | 2.00×10⁻⁶ |
| 7 | 10⁻⁷ | 4.18×10⁻⁸ | 1.94×10⁻¹⁰ | 2.16×10⁻⁷ |
| 8 | 10⁻⁸ | 2.97×10⁻⁹ | 2.58×10⁻⁹ | 1.83×10⁻⁷ |
| 9 | 10⁻⁹ | 5.25×10⁻⁸ | 2.97×10⁻⁹ | 1.68×10⁻⁶ |
| 10 | 10⁻¹⁰ | 5.85×10⁻⁸ | 5.85×10⁻⁸ | 1.51×10⁻⁵ |
| 11 | 10⁻¹¹ | 1.17×10⁻⁶ | 1.17×10⁻⁶ | 1.50×10⁻⁴ |
| 12 | 10⁻¹² | 4.32×10⁻⁵ | 1.23×10⁻⁵ | 1.69×10⁻³ |
| 13 | 10⁻¹³ | 7.34×10⁻⁴ | 1.79×10⁻⁴ | 1.67×10⁻² |
| 14 | 10⁻¹⁴ | 3.71×10⁻³ | 3.71×10⁻³ | 1.51×10⁻¹ |
| 15 | 10⁻¹⁵ | 1.48×10⁻² | 1.48×10⁻² | 1.73×10⁰ |
| 16 | 10⁻¹⁶ | 5.40×10⁻¹ | 1.48×10⁻² | 1.71×10¹ |
h = 10⁻¹
Forward error 4.29×10⁻² (budget: truncation 5.00×10⁻², rounding 4.30×10⁻¹⁵); centered error 9.00×10⁻⁴.
from −3 to 3, up to 2 decimal places
from 1 to 16, whole numbers only
For at , the forward difference with is off by about and the centered difference by about . Drag toward smaller : both errors fall at first, reach a minimum, then rise. At the forward estimate is wrong in the third digit. Exact algebra says smaller is better; floating-point arithmetic disagrees.
Intuition: two errors pulling in opposite directions
A difference quotient replaces a limit by a finite step, which costs truncation error: the secant is not the tangent. Shrinking reduces it. But and are each rounded to about 16 significant digits; when is tiny they agree in most of those digits, their difference keeps only a few correct ones, and dividing by magnifies the loss. That rounding error grows as shrinks. The total is smallest somewhere in between.
Definition
With step :
These are the forward, backward and centered differences. Each approximates ; their errors are measured here against an independent reference: the analytic derivative ( for , for , and the exact rational for the cubic).
Mechanics: what the explorer computes
All quotients are computed in IEEE binary64, exactly as naive code would, dividing by the nominal . For each scheme and each the plot shows of the observed error. The dashed budget curves add the leading truncation term and a rounding term built from machine epsilon and the sizes of the sampled values. The budgets are explanations and upper-envelope estimates for these smooth functions, not guarantees for arbitrary .
Derivation: the two terms and the best step
Taylor’s theorem gives , so
a truncation error of order .
For the centered difference the terms cancel and the error is — order . Each computed value carries a relative rounding error of about , so the computed numerator is off by roughly , divided by . Minimising
gives , about for well-scaled ; for the centered difference , about .
Worked example
, , :
- : forward error , centered — truncation dominates;
- : forward , centered ;
- : forward , the best region for forward differences;
- : forward — rounding has taken over.
Replay the worked derivation
h = 10⁻¹
Forward error 4.29×10⁻² (budget: truncation 5.00×10⁻², rounding 4.30×10⁻¹⁵); centered error 9.00×10⁻⁴.
When numbers mislead
At , may round to itself, making — a confident, completely wrong answer. Even the step is inexact: is usually not exactly in binary64, a representation error the budget includes. For a function with a corner or a jump, no step size gives a meaningful derivative, because none exists (III.3).
Executable reference code
The reference compares the three schemes with an exact derivative of a polynomial evaluated in floating point and in Fraction, isolating rounding error. The NumPy version sweeps , finds the empirical best step and compares it with .
"""Finite differences of a polynomial: exact rational quotients versus binary64 ones isolate rounding error (III.14)."""
from fractions import Fraction as F
p = lambda x: x**3 / 6 - x # works for Fraction and float
dp = lambda x: x**2 / 2 - 1 # exact derivative
x = F(3, 2)
for k in range(1, 17):
h = F(1, 10**k)
exact_forward = (p(x + h) - p(x)) / h # exact rational quotient
# In exact arithmetic the forward error is exactly h*x/2 + h**2/6 (truncation only).
assert exact_forward - dp(x) == h * x / 2 + h * h / 6
xf = 1.5
errs = [abs((p(xf + 10.0**-k) - p(xf)) / 10.0**-k - float(dp(x))) for k in range(1, 17)]
best = min(range(16), key=lambda i: errs[i])
assert 5 <= best + 1 <= 10 # the best step is near sqrt(eps) ~ 1e-8, not the smallest one
assert errs[15] > errs[best] * 100 # h = 1e-16 is far worse than the best step"""Sweep h = 10**-1 ... 10**-16 for sin at 1: forward error is smallest near sqrt(eps), centered near eps**(1/3) (III.14)."""
import numpy as np
x = 1.0
h = 10.0 ** -np.arange(1, 17)
forward = np.abs((np.sin(x + h) - np.sin(x)) / h - np.cos(x))
centered = np.abs((np.sin(x + h) - np.sin(x - h)) / (2 * h) - np.cos(x))
eps = np.finfo(float).eps
assert abs(np.log10(h[np.argmin(forward)]) - np.log10(np.sqrt(eps))) <= 1.5
assert abs(np.log10(h[np.argmin(centered)]) - np.log10(np.cbrt(eps))) <= 1.5
assert centered.min() < forward.min() # higher order wins at its best step
assert forward[-3] > 100 * forward.min() # tiny steps are much worseExplore
Compare eˣ at 2 with sin x: the larger , the larger the rounding term and the earlier the error curve turns up. On the polynomial, the centered difference’s truncation error is tiny; its curve is shaped almost entirely by rounding. Notice where forward and backward errors differ in sign but not in size.
Common mistakes
Choosing as small as possible. Using a fixed absolute for inputs of very different magnitude (a relative step such as is usual). Trusting a finite-difference check of a gradient without a tolerance that reflects these errors.
Numerical considerations
Complex-step differentiation, Richardson extrapolation and automatic differentiation (III.10) avoid or reduce the cancellation problem. When finite differences are used as a test of AD code, a centered difference with – and a relative tolerance around is a common, defensible choice for smooth functions.
Engineering and ML connections
Gradient checking in machine-learning code compares backpropagation with centered differences. Simulation codes estimate Jacobians by finite differences when no derivative code exists, and must choose step sizes per variable. The same truncation-versus-rounding trade-off appears whenever a continuous limit is replaced by a finite, floating-point computation.
Exercises
Summary and checkpoints
Forward and backward differences have truncation error of order , centered differences of order ; rounding error grows like . Their sum is minimised near (forward) or (centered), and tinier steps make results worse. Next, III.15 studies the matching question for integrals.