My question is bolded within the text below.
Memoization speeds up a recursion that needs to recall a prior rule other than the last one calculated. For instance, on my machine, the following take 0.0003 s and 7 s, respectively, each on fresh kernels (the first code block is with memoization, and the second is without):
fMem1[1] = 1; fMem1[2] = 1;(* run on fresh kernel *)
fMem1[x_] := fMem1[x] = fMem1[x - 1]^2 - fMem1[x - 2] + 1;
t = AbsoluteTime[];
fMem1[33];
AbsoluteTime[] - t
f1[1] = 1; f1[2] = 1;(*run on fresh kernel*)
f1[x_] := f1[x - 1]^2 - f1[x - 2] + 1;
t = AbsoluteTime[];
f1[33];
AbsoluteTime[] - t
This makes sense since, to calculate f(n), Mathematica needs both f(n-1) and f(n-2). Yet, in the absence of memoization, all that is stored is the previously-calculated rule for f(n), which is now f(n-1). All those before it have been overwritten which, in this case, means Mathematica has to repeatedly recalculate f(n-2).
Conversely, and consistent with the above, when the recursion requires only a single call to the previously-calculated rule, memoization provides no speed benefit (unless, of course, you need to rerun the recursion on the existing kernel, in which case memoization's rule caching will obviate the need to recalculate prior rules). For instance, the times for both of these are 3 s:
fMem2[1] = 1;(* run on fresh kernel *)
fMem2[x_] := fMem2[x] = 2*fMem2[x - 2]^3+1;
t = AbsoluteTime[];
fMem2[37];
AbsoluteTime[] - t
f2[1] = 1; (* run on fresh kernel *)
f2[x_] := 2*f2[x - 2]^3+1;
t = AbsoluteTime[];
f2[37];
AbsoluteTime[] - t
Why, then, does memoization provide a speed-up when the recursion only requires the previously-calculated rule, but that rule is called more than once (i.e., is present in more than one term)? It's as if the cached rule for the previous f(n) (which, here, is now f(n-1)) is overwritten after the first time it is used in an expression. Here the version with memoization takes 0.0003 s; that without takes 34 s:
fMem3[1] = 1;(* run on fresh kernel *)
fMem3[x_] := fMem3[x] = fMem3[x - 1]^2 - fMem3[x - 1] + 1;
t = AbsoluteTime[];
fMem3[25];
AbsoluteTime[] - t
f3[1] = 1;(* run on fresh kernel *)
f3[x_] := f3[x - 1]^2 - f3[x - 1] + 1;
t = AbsoluteTime[];
f3[25];
AbsoluteTime[] - t
f3[x_] := f3[x - 1]^2 - f3[x - 1] + 1
requires two calls tof3[x - 1]
, which require four calls tof3[x - 2]
, etc. exponentially. This is becausef3[x_]
is defined with a delayed assignment and might have side-effects, so cannot be auto-cached by the kernel. If you definecounter = 0; f3[x_] := (counter++; f3[x - 1]^2 - f3[x - 1] + 1);
then after callingf3[25]
we havecounter
equal to $16777215=2^{24}-1$, showing the exponential calling pattern. $\endgroup$f[x_]:= f[x-1] +f[x-1]+1
, but not withf[x_]:= f[x-1]+1
. It sounds like you're saying that memoization speeds up the recursion in both cases, because prior values are never cached in its absence. Thus withf[x_]:= f[x-1] +1
, a call is required tof[x-1]
, which in turn requires a call tof[x-2]
, and so on. With memoization, by contrast, such recalculation would not be needed. $\endgroup$f[x_]:= f[x-1]+f[x-1] +1
, the advantage of memoization is far less, which is why I don't see the timing difference. Do I have that right? If so, it seems that, if f[x] were a temporally expensive calculation, then I should see a timing difference even with just one call. i.e., even in the absence of an exponential growth in the number of required calculations.Integrate[1 /( Sinh[z] ), {z, 1, 10}]
is relatively expensive, so I compared $\endgroup$