5
$\begingroup$

I was recently introduced to the LetL macro thanks to Leonid's answer to one of my prior questions. I was, needless to say, impressed by the simplicity of its recursive definition. However, I noticed that it may not necessarily be optimized. As it is defined, if my LetL statement contains a definition which does not need to be nested, then it will call With unnecessarily:

testLetL := LetL[{x = 1, y = 2, z = 2 x y}, {x, y, z}]
?testLetL
(* testLetL:=With[{x=1},With[{y=2},With[{z=2 x y},{x,y,z}]]] *)

So I compared it to Module:

testModule := Module[{x = 1, y = 2, z}, z = 2 x y; {x, y, z}]
(Do[#, {i, 5000000}] // AbsoluteTiming) & /@ {testLetL, testModule}
(* {{0.9390537, Null}, {0.9270530, Null}} *)

As you can see, there doesn't seem to be much speed gained in using LetL - essentially nested Withs - instead of Module. I thought perhaps that it was the extra With being called that was slowing things down. So I tried another test:

testLetL2 := LetL[{x = 1, y = 2 x }, {x, y}]
testModule2 := Module[{x = 1, y}, y = 2 x ; {x, y}]
(Do[#, {i, 5000000}] // AbsoluteTiming) & /@ {testLetL2, testModule2}
(* {{0.9270531, Null}, {0.9120521, Null}} *)

This again showed that they were pretty much the same, if not Module being a bit faster.


My question is, then:

Is LetL simply used for convenience or are my tests missing something?

$\endgroup$

2 Answers 2

8
$\begingroup$

The main point of LetL is just replacement of nested With, not necessarily the speed gain. Now, why would one want to use nested With in place of Module:

  • Immutable code (same advantages as With - no side effects in the body)
  • Use variables defined earlier in definitions of variables defined later.

In fact, if you want the second property, you will either have to have nested Module-s as well, or make side effects in the body.

That said, LetL should be pretty fast. You should not normally see large timing difference between LetL and equivalent nested With. Moreover, for functions defined via SetDelayed, LetL expands into equivalent nested With at definition-time, so there is no run-time performance penalty at all.

And yes, your tests missed the point, since pure function is #-& notation evaluates its argument, so you were actually testing already evaluated expressions. Try this:

Do[testLetL2,{i,50000}]//AbsoluteTiming
Do[testModule2,{i,50000}]//AbsoluteTiming

(*
   {0.190430,Null}
   {0.276367,Null}
*)
$\endgroup$
2
  • $\begingroup$ Ah! I knew those results were suspicious. $\endgroup$
    – VF1
    Oct 4, 2012 at 22:26
  • $\begingroup$ @VF1 in the future I suggest you define "active" code like this: testLetL2[] := LetL[{x = 1, y = 2 x }, {x, y}] -- now you can pass around testLetL2 without accidental evaluation. To use it just call testLetL2[] or for example (Do[#[], {i, 5*^6}] // AbsoluteTiming) & /@ {testLetL2, testModule2} $\endgroup$
    – Mr.Wizard
    Oct 4, 2012 at 22:35
1
$\begingroup$

I'd like to add to this post that there are performance gains to be found in intelligently positioning you'r nested With statements. To take your example if we have the following definitions and code:

 {x = 1, y = 2, z = 2 x y}, {x, y, z}

We can either use a Module, naively use 3 nested With's, or though analyzing the structure of the definitions we can see that we can get away with just two Withs, and then we can be really smart about our optimization (to be explained after performance results for dramatic effect!). So here's the results of these methods:

testModule := Module[{x = 1, y = 2, z}, z = 2 x y; {x, y, z}];
testLet := With[{x = 1}, With[{y = 2}, With[{z = 2 x y}, {x, y, z}]]];
testImprovLet := With[{x = 1, y = 2}, With[{z = 2 x y}, {x, y, z}]];

On my system, calling these three and the to be described testSuperLet gives the following results:

Graph of performance numbers {1.1,0.89,0.5,0.02}

So in short, With is faster than Module for this test, and trying to minimize the levels of With's does help improve the speed significantly. But all of these are nothing compared to superLet, so let's get to it. What is `superLet? Well if you look at the test again, you'll notice that we don't actually need to recalculate anything since it's just providing a constant, so the best way to nest the with's is to just move them left of the definition:

With[{x = 1, y = 2}, With[{z = 2 x y}, testSuperLet := {x, y, z}]]

I know I know, it looks like cheating, however in many cases, using your definitions outside of the Set/SetDelayed can provide significant improvements in speed with only a slight cost in DownValue size and complexity.

The code used to generate the timing graph above:

SetAttributes[time,HoldAll]
time[code_,num_]:=(Do[code,{num}];//AbsoluteTiming//First)
bar[n_,v_]:=Overlay[{Pane["",BaseStyle-> Background->Orange,ImageSize-> Scaled[v]],Pane[n]}]

SetAttributes[timeTableView,HoldAll]
timeTableView[calls___,num_Integer]:=Module[{labels,times},
labels=List@@(Function[a,SymbolName[Unevaluated[a]],HoldAll]/@Hold[calls]);
times=List@@(time/@Hold[calls]);
Labeled[
Column[bar[ToString[#[[2]]]<> " seconds - "<>#[[1]],#    [[3]]]&/@({labels,Round[times,0.01],times/Max[times]}\[Transpose]),ItemSize-> {30}],
ToString[num]<>" Calls performed",Top]
]

timeTableView[testModule, testLet, testImprovLet, testSuperLet, 200000]
$\endgroup$
7
  • 2
    $\begingroup$ Your testSuperLet is really misleading in this context, since you were comparing the time to execute various nested scoping constructs, and your testSuperLet is from a completely different area of optimization (which is, memoization, caching, and pre-processing / precomputing results). I agree with the rest of your analysis, but I think that relative performance of various scoping constructs should not be mixed with other forms of optimization, to maintain a clean presentation logic. $\endgroup$ Jan 27, 2013 at 22:28
  • $\begingroup$ @LeonidShifrin My own scoping macro currently does this optimization, so I don't think it's that unfair to compare performance. It's really the best way to order the scopes if you are doing definition time optimization, which is also what is typically done in the letmacro. And "completely different area" is kind of rough remembering that it's just a case of moving the lhs two levels into the definition, other then that the code is practically identical. $\endgroup$
    – jVincent
    Jan 27, 2013 at 22:38
  • 1
    $\begingroup$ This is not what was the topic of this question. If you have a more advanced optimizing macro, it would have been more appropriate to discuss it in the context of similar optimization techniques I mentioned, or in a self-answered question. Discussing it in the rather artificial context of micro-benchmarking nested scoping constructs hardly seems logical. On a practical note, my experience is that the kind of optimization you discuss is hard to automate in general, especially in a dynamic language like Mathematica, but is rather easy to do manually for a given particular problem. $\endgroup$ Jan 27, 2013 at 22:48
  • 2
    $\begingroup$ This is my last comment here. You seem to not see a difference between measuring efficiency of a single call of various (possibly nested) scoping constructs - which is a purely technical issue telling us something about relative efficiencies of them for a single call, and discussing methods of how to optimize via reducing the number of needed calls. I don't argue that what you discussed is a valid optimization technique. I just say that you've chosen a rather inappropriate context to illustrate it, and ended up comparing apples and oranges, in my opinion. $\endgroup$ Jan 27, 2013 at 23:00
  • 2
    $\begingroup$ @jVincent scoping isn't just syntactic sugar--sometimes it's needed for correctness. Obviously the fastest scoping construct is one that you can avoid using at all, but unless everyone is going to adopt a purely functional style, I have to agree with Leonid about the appropriateness of the comparison. That being said, I'd be very interested to see (ideally, in its own self-answered question) your optimizing macro if you think you've found a sufficiently general method of resolving dependencies. $\endgroup$ Jan 28, 2013 at 2:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.