Skip to main content

timing - Throwaway memoization makes built-ins faster?


Yesterday I got into an argument with @UnchartedWorks over in the comment thread here. At first glance, he posted a duplicate of Marius' answer, but with some unnecessary memoization:


unitize[x_] := unitize[x] = Unitize[x]
pick[xs_, sel_, patt_] := pick[xs] = Pick[xs, sel, patt]

and proposed the following test to justify his claim that his approach is faster:


RandomSeed[1];

n = -1;
data = RandomChoice[Range[0, 10], {10^8, 3}];

AbsoluteTiming[Pick[data, Unitize@data[[All, n]], 1] // Length]

AbsoluteTiming[pick[data, unitize@data[[All, n]], 1] // Length]

(*
{7.3081, 90913401}
{5.87919, 90913401}

*)

A significant difference. Naturally, I was skeptical. The evaluation queue for his pick is (I believe) as follows:



  1. pick is inert, so evaluate the arguments.

  2. data is just a list, 1 is inert, data[[All, n]] quickly evaluates to a list

  3. unitize@data[[All, n]] writes a large DownValue...

  4. ...calling Unitize@data[[All, n]] in the process, returning the unitized list.

  5. Another large DownValue of the form pick[data] = *pickedList* is created (data here is, of course, meant in its evaluated form), never to be called again (unless, for some reason, we explicitly type pick[data]).

  6. The *pickedList* is returned.



What about the evaluation queue for Pick[data, Unitize@data[[All, n]], 1]?



  1. Pick is inert.

  2. data becomes an inert list, 1 is inert, data[[All, n]] quickly evaluates to an inert list.

  3. Nothing happens here.

  4. Unitize@data[[All, n]] returns the unitized list.

  5. Nothing happens here either.

  6. The same step as before is taken to get us the picked list.



So, clearly pick has more things to do than Pick.


To test this out I run the following code:


Quit[]

$HistoryLength = 0;

Table[
Clear[pick, unitize, data];
unitize[x_] := unitize[x] = Unitize[x];
pick[xs_, sel_, patt_] := pick[xs] = Pick[xs, sel, patt];

data = RandomChoice[Range[0, 10], {i*10^7, 3}];
{Pick[data, Unitize@data[[All, -1]], 1]; // AbsoluteTiming // First,
pick[data, unitize@data[[All, -1]], 1]; // AbsoluteTiming // First},
{i, 5}]

Much to my surprise, pick is consistently faster!



{{0.482837, 0.456147},
{1.0301, 0.90521},
{1.46596, 1.35519},

{1.95202, 1.8664},
{2.4317, 2.37112}}

How can I protect myself from black magic make a representative test? Or should I embrace the black magic is this real and a valid way to speed things up?


Update re: answer by Szabolcs


Reversing the order of the list like so:


{pick[data, unitize@data[[All, -1]], 1]; // AbsoluteTiming // First,
Pick[data, Unitize@data[[All, -1]], 1]; // AbsoluteTiming // First}

gave me the following result:




{{0.466251, 0.497084},
{1.18016, 1.17495},
{1.34997, 1.42752},
{1.80211, 1.93181},
{2.25766, 2.39347}}

Once again, regardless of order of operations, pick is faster. Caching could be suspect, and as mentioned in the comment thread of the other question, I did try throwing in a ClearSystemCache[] between the pick and Pick, but that didn't change anything.


Szabolcs suggested that I throw out the memoization and just use wrapper functions. I presume, he meant this:


unitize[x_] := Unitize[x];

pick[xs_, sel_, patt_] := Pick[xs, sel, patt];

As before, on a fresh kernel I set history length to 0 and run the Table loop. I get this:


{{0.472934, 0.473249},
{0.954632, 0.96373},
{1.42848, 1.43364},
{1.91283, 1.90989},
{2.37743, 2.40031}}

i.e. nearly equal results, sometimes one is faster, sometimes the other (left column is pick, right is Pick). The functions perform as well as Pick in a fresh kernel.



I try again with the memoization as described towards the beginning of the answer:


{{0.454302, 0.473273},
{0.93477, 0.947996},
{1.35026, 1.4196},
{1.79587, 1.90001},
{2.24727, 2.38676}}

The memoized pick and unitize perform consistently better out of a fresh kernel. Of course, it uses twice the memory along the way.



Answer



Cause of speed up



This is definitely not memoization. The reason for the observed speed up is that for large arrays (e.g. 10^8 elements), the memory clean up operations may take noticeable time. If one doesn't free memory, one can perform some operations a bit faster.


Here is a simple example:


Let's create a large array, then perform a calculation, and remove the array:


AbsoluteTiming[
Total[ConstantArray[0, 10^8]];
]


{0.422509, Null}




It takes 0.42 seconds. Let's now do the same thing, but keep the array in memory:


AbsoluteTiming[
Total[garbage = ConstantArray[0, 10^8]];
]


{0.366755, Null}



This evaluation is a bit faster.


Let's check how long does it take to remove the large array:



AbsoluteTiming[
Remove[garbage]
]


{0.061982, Null}



Note that 0.06 seconds is the difference of the calculation times above. This example shows that if we keep the large array instead of removing it, our code can run faster, because we don't need to spent time on freeing memory.


Your example


In the example you provide, removing the result of Unitize@data[[All, n]] from memory takes some time. If one saves this array in a redundant variable, one avoids immediate memory clean-up and the evaluation seems to be faster. In case of pseudo-memoization the Clear[pick, unitize] command will take extra time to free the memory, but this command is placed outside the AbsoluteTiming[] scope. That is why "memoization" seems to speed up the calculation.



How to make a representative test?


You should put Clear[pick, unitize] inside your timing function. This test will show that the pseudo-memoization technique is actually slower than built-in functions:


Table[
Clear[data];
data=RandomInteger[{0,10},{i*10^7,3}];
{
Pick[data,Unitize@data[[All,-1]],1]; // AbsoluteTiming // First
,
Clear[pick,unitize];
unitize[x_]:=unitize[x]=Unitize[x];

pick[xs_,sel_,patt_]:=pick[xs,sel,patt]=Pick[xs,sel,patt];
pick[data,unitize@data[[All,-1]],1]; // AbsoluteTiming // First
},
{i,5}]

(*
{{0.534744, 0.469538},
{1.03776, 1.05842},
{1.58536, 1.65404},
{2.10422, 2.11284},

{2.48129, 2.71405}}
*)

Technical note: as noted by Carl Woll in comments, if one wants to measure the symbol-removing-time using the following code:


In[1] := garbage = ConstantArray[0, 10^8];
In[2] := AbsoluteTiming[Remove[garbage]]

one should set $HistoryLength to zero, otherwise the Out[1] variable will retain the contents of the large array. If Out[1] retains the large data, Remove[garbage] will only delete the reference, but not the data itself. Deletion time of a reference is almost zero, but it doesn't correspond to the deletion time for large data.


Comments

Popular posts from this blog

plotting - How to draw lines between specified dots on ListPlot?

I would like to create a plot where I have unconnected dots and some connected. So far, I have figured out how to draw the dots. My code is the following: ListPlot[{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {1, 4}, {2, 5}, {3, 6}, {4, 7}, {1, 7}, {2, 8}, {3, 9}, {4, 10}, {1, 10}, {2, 11}, {3, 12}, {4,13}, {2.5, 7}}, Ticks -> {{1, 2, 3, 4}, None}, AxesStyle -> Thin, TicksStyle -> Directive[Black, Bold, 12], Mesh -> Full] I have thought using ListLinePlot command, but I don't know how to specify to the command to draw only selected lines between the dots. Do have any suggestions/hints on how to do that? Thank you. Answer One possibility would be to use Epilog with Line : ListPlot[ {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {1, 4}, {2, 5}, {3, 6}, {4, 7}, {1, 7}, {2, 8}, {3, 9}, {4, 10}, {1, 10}, {2, 11}, {3, 12}, {4, 13}, {2.5, 7}}, Ticks -> {{1, 2, 3, 4}, None}, AxesStyle -> Thin, TicksStyle -> Directive[Black, Bold, 12], Mesh -> Full, Epilog -> { Line[ ...

equation solving - Invert and fit implicitly defined curve

I need to fit an implicitly defined curve. I thought I could get some data out of Solve , and then using FindFit . Therefore, I would like to find the relation the parametric curve defined by $F(x,y)=0$: Solve[-(1/2) + 1/2 (0.41202 BesselK[0, 0.1 Sqrt[x^2 + y^2]] + (0.101483 x BesselK[1, 0.1 Sqrt[x^2 + y^2]])/Sqrt[x^2 + y^2]) == 0, y] But I can't get an output: Solve was unable to solve the system with inexact coefficients or the system obtained by direct rationalization of inexact numbers present in the system. Since many of the methods used by Solve require exact input, providing Solve with an exact version of the system may help. >> Edit: In particular, I would like to fit the data coming from the curve with the expression of another curve, and not with a function $f(x)$. In particular, since this clearly looks like a cardioid , I would like it to fit to something like it. What other strategies could I try?

dynamic - How can I make a clickable ArrayPlot that returns input?

I would like to create a dynamic ArrayPlot so that the rectangles, when clicked, provide the input. Can I use ArrayPlot for this? Or is there something else I should have to use? Answer ArrayPlot is much more than just a simple array like Grid : it represents a ranged 2D dataset, and its visualization can be finetuned by options like DataReversed and DataRange . These features make it quite complicated to reproduce the same layout and order with Grid . Here I offer AnnotatedArrayPlot which comes in handy when your dataset is more than just a flat 2D array. The dynamic interface allows highlighting individual cells and possibly interacting with them. AnnotatedArrayPlot works the same way as ArrayPlot and accepts the same options plus Enabled , HighlightCoordinates , HighlightStyle and HighlightElementFunction . data = {{Missing["HasSomeMoreData"], GrayLevel[ 1], {RGBColor[0, 1, 1], RGBColor[0, 0, 1], GrayLevel[1]}, RGBColor[0, 1, 0]}, {GrayLevel[0], GrayLevel...