# Composition vs serial assignment, which performs better?

I have a giant block of code that manipulations an expression in many different ways. Currently it looks like

f[input_] := Module[{...},

expr = stuff[input];
expr = moreStuff[expr];
expr = evenMoreStuff[expr];
expr = evenEvenMoreStuff[expr];
Return[expr];
]


where all the stuff, moreStuff, ... are actually things like replacements and series and derivatives which themselves are many lines long. (and I realize the Return is unnecessary; it's there for emphasis).

I remember reading somewhere that everytime I use =, Mathematica copies an expression in the memory (which may be slow and inefficient). Would my code go faster / be more memory efficient if I rewrote my code so that I essentially nest everything together like this?

f[input_] := Module[{...},
Return[evenEvenMoreStuff[evenMoreStuff[moreStuff[stuff[input]]]]];
]

• I'm also interested in the general answer to this, but have you tested your code, and if so, what were the results? Commented Jul 10, 2015 at 18:43
• I believe a better title could be Efficiency of a = f[g[x]] vs (a = g[x]; a = f[a]) Composition vs serial assignment Commented Jul 10, 2015 at 19:09
• @belisarius yes that is a better title; it is changed. Commented Jul 10, 2015 at 19:29
• @march I haven't tried it by composing everything together. It would take me a very long time. So I thought I would ask on here before starting... (and besides if I tried it, I would get the answer; so why would I bother asking?) Commented Jul 10, 2015 at 19:30
• "I remember reading somewhere that everytime I use =, Mathematica copies an expression in the memory (which may be slow and inefficient)." <-- that's not true here. Mathematica only copies expressions when you modify then. Example: a={1,2,3}. Now b=a involves no copy at all. a and b use the same memory area to store their contents. But to you, the user, they do appear to behave as separate arrays. To achieve this, as soon as you modify only a subpart of either a or b, e.g. a[[1]] = 42, an extra copy is created to allow the two arrays to differ. Commented Jul 10, 2015 at 19:35

I remember reading somewhere that everytime I use =, Mathematica copies an expression in the memory (which may be slow and inefficient).

This is not quite true, as written here. Mathematica uses a copy-on-write behaviour, i.e. it will only create an actual copy of a datastructure if you modify it.

Example:

a = {1,2,3};


As this is evaluated, first the array {1,2,3} is created in memory. Then it is assigned to the variable a. This assignment does not involve copying the array.

Now let's assign the value of a to b:

b = a


Once again, the array stays put in the memory, and a separate copy is not created for b.

But we know that in Mathematica a and b are really treated as two separate arrays. Unlike in languages such as Python, modifying a does not modify b and vice versa. To achieve this behaviour, Mathematica will create a copy of the array when a part of the array is modified:

b[[1]] = 42;


This is the step that triggers creating an extra copy.

Copy-in-write is just an optimization technique that allows the system to behave as if all variables pointed to different memory locations, without actually creating copies when not necessary.

So the second version of f should not use any more memory than the first one.

But it will run more slowly because of the extra operations that need to be carried out: assigning a value to the symbol expr.

Example:

stuff[x_] := x
moreStuff[x_] := x
evenMoreStuff[x_] := x
evenEvenMoreStuff[x_] := x

f1[input_] := Module[{expr}, expr = stuff[input];
expr = moreStuff[expr];
expr = evenMoreStuff[expr];
expr = evenEvenMoreStuff[expr];
Return[expr];]

f2[input_] :=
Module[{},
Return[evenEvenMoreStuff[evenMoreStuff[moreStuff[stuff[input]]]]]]

Timing@Do[f1[i], {i, 1*^6}]
(* {5.772924, Null} *)

Timing@Do[f2[i], {i, 1*^6}]
(* {2.476713, Null} *)


Keep in mind that I have chosen the functions so that they do as little as possible. They just return their arguments. This allows us to measure the small amount of time assignment takes. If they function actually do something useful, the slowdown due to the assignments will in most cases be negligible compared to that.

Return and Module itself also take some time.

f3[input_] :=
evenEvenMoreStuff[evenMoreStuff[moreStuff[stuff[input]]]]

Timing@Do[f3[i], {i, 1*^6}]
(* {2.075384, Null} *)