15
$\begingroup$

Is there a reason to use Hold* attributes for functional code (e.g. no intention to mutate input)? I'd expect performance gains as in pass by value vs pass by reference.

E.g.

data = RandomReal[1, 10^8];

data // Function[x, x[[1]]] // RepeatedTiming
{8.*10^-7, 0.0372378}
data // Function[x, x[[1]], HoldAllComplete] // RepeatedTiming
{8.0*10^-7, 0.0372378}

The question is, why doesn't it matter? Or are there cases where it matters, performance tuning wise.

Due to my limited understanding of internals of WL implementation I am puzzled by this and I often have a case where I need to traverse a big expression by keeping a 'reference' to the expression as a whole. So I can do:

parse[bigExpression]:=parse[bigExpression, #] & /@ bigExpression

or I can make parese a HoldFirst. Or I could even MapIndexed and keep track of the index I could use to access a global bigExpression.

Edit:

As noted in comments, the initial example wasn't the best, here I hope is a better one:

data = RandomReal[1, 10^8];

g1[x_] := x[[1]];
f1[x_] := g1[x];

SetAttributes[{g2, f2}, HoldAll];
g2[x_] := x[[1]];
f2[x_] := g2[x];
f1[data] // RepeatedTiming
f2[data] // RepeatedTiming
%%/%

{8.38*10^-7, 0.487565}

{7.840*10^-7, 0.487565}

{1.07, 1.}

$\endgroup$
16
  • 1
    $\begingroup$ @LukasLang I appreciate better examples, this was the simplest that came to my mind. $\endgroup$
    – Kuba
    Apr 10 '19 at 15:43
  • 2
    $\begingroup$ Also, I believe that there are times when using the Hold attribute prevents mathematica from copying data, which can be important when working with large datasets when there is a risk of running out of RAM or hitting the swap memory limit. However, I found it difficult to find good examples of this. $\endgroup$ Apr 10 '19 at 15:44
  • 1
    $\begingroup$ @Somos I am not sure what concept it uses but if you write your code in a sloppy way 'copying' will make it slower. $\endgroup$
    – Kuba
    Apr 10 '19 at 15:44
  • 1
    $\begingroup$ The documentation for HoldAllComplete[] has an example using the Hofstadter-Conway sequence which shows why it is sometimes faster to use it. $\endgroup$
    – Somos
    Apr 10 '19 at 15:50
  • 4
    $\begingroup$ As b3m2a1 pointed out, Mathematica uses layz copying. This why I think that Hold-Attributes give you the edge only if you want to modify data in place. Then it will safe you a couple of copy operations and, probably more importantly, a lot of memory allocation and deallocation under the hood. Hold-attributes also disable pattern matching which can also be a reason why, at times Hold-Attributes might result in faster code: They enforce you to aviod complicated patterns in a function definition. $\endgroup$ Oct 19 '19 at 12:32
7
$\begingroup$

I don't understand why you think there will be a difference in timing for your example. In both cases the head is evaluated, the data variable is evaluated, the Function is evaluated, and then the Part is evaluated. It's just a matter of order. Let's take a smaller example so that we can follow the trace:

data = RandomReal[1, 3];

f1 = Function[x, x[[1]]];
f2 = Function[x, x[[1]], HoldAllComplete];

The first example trace:

Trace[data //f1]
{
{f1,Function[x,x[[1]]]},
{data,{0.717387,0.557898,0.102441}},
Function[x,x[[1]]][{0.717387,0.557898,0.102441}],
{0.717387,0.557898,0.102441}[[1]],
0.717387
}

(the HoldForm wrappers have been suppressed)

Notice how the head is evaluated, data is evaluated, the Function is evaluated, and then finally Part is evaluated.

The second example:

Trace[data //f2]
{
{f2,Function[x,x[[1]],HoldAllComplete]},
Function[x,x[[1]],HoldAllComplete][data],
data[[1]],
{data,{0.717387,0.557898,0.102441}},
{0.717387,0.557898,0.102441}[[1]],
0.717387
}

In this case, the head is evaluated, the Function is evaluated, data is evaluated, and then Part is evaluated. The HoldAllComplete attribute only changes the order of evaluation, it doesn't eliminate any evaluations.

$\endgroup$
1
  • $\begingroup$ You are right, my example was too minimal. I added a new one. $\endgroup$
    – Kuba
    Oct 21 '19 at 7:04
5
+200
$\begingroup$

In general, as Henrik and I note in the comments, Mathematica makes sure only to copy data when it has undergone some sort of change internally. An easy way to see this is to set a flag like Valid and see when it disappears:

myBigData = RandomReal[{}, {500, 800}];
myBigData // System`Private`SetValid;

amIDifferentNowHeld[Hold[data_]] :=
 ! System`Private`ValidQ[data]
amIDifferentNow[data_] :=
 ! System`Private`ValidQ[data]

amIDifferentNow[myBigData]

False

amIDifferentNowHeld[Hold[myBigData]]

False

trueCopy[data_] :=
  BinaryDeserialize[BinarySerialize[data]];

amIDifferentNow[trueCopy[myBigData]]

True

So it seems pretty clear to me that we were working with the same object in both the unheld and held cases and this is why you can pass big arrays and stuff through a program and not need to worry about pass-by-value or pass-by-reference. It's really all pass-by-reference anyway with a pointer to some kind of internal Expression object.

Now, where Hold does buy you something is in data-safety. Consider this kind of naive data cleaning you might think to do (basically copied from some old code I wrote). I wanted to canonicalize some options and merge with some defaults so that they were a nice single Association I could store and query later and feed through my program. Here's how that looked:

$defaults = {
   {
    "GridOptions" -> {"Domain" -> {{-5, 5}, {-5, 5}}, "Points" -> {60, 60}},
    "PotentialEnergyOptions" -> {"PotentialFunction" -> (1/2 #^2 &)},
    "KineticEnergyOptions" -> {"Masses" -> {1, 1}, "HBars" -> {1, 1}}
    }
   };
Options[doADVR] = {
   "KineticEnergyMatrix" -> None,
   "PotentialEnergyMatrix" -> None
   };
getWfs[ops : OptionsPattern[]] :=

 Module[{dom, pts, potF, m, hb, ke, pe, opts},
  opts = Merge[
    {
     Normal@{ops},
     $defaults
     },
    First
    ];
  (* do the real calculation, usually, but that's not the point here *)

  opts
  ]

And here's what happens when you feed in a SparseArray for one of the "*Matrix" arguments:

ke = $H4DDVR["KineticEnergy", "Points" -> {30, 30}];

ke // Head
ke // Dimensions

SparseArray

{900, 900}

dvrOpts = getWfs["KineticEnergyMatrix" -> ke];

dvrOpts["KineticEnergyMatrix"] // Head
dvrOpts["KineticEnergyMatrix"] // Dimensions

List

{900, 900}

We can see that Normal converted it to a List! As these things grow in size the penalty for that gets more and more dramatic.

And Mathematica provides lots of ways for you to shoot yourself in the foot like this: Normal recurses into data structures, ReplaceAll digs into even PackedArray structures, etc. and those are the well designed functions. Too much of the system tries to be "clever" or "helpful" and in doing so makes it really easy to wreck your data by accident.

Obviously Hold has its core usage in clever destructing-based meta-programming, but in terms of performance a big place Hold can help you out is in making sure your data flows through a program unadulterated, e.g.:

dvrOpts["KineticEnergyMatrix"] // Head
ReleaseHold@dvrOpts["KineticEnergyMatrix"] // Head
ReleaseHold@dvrOpts["KineticEnergyMatrix"] // Dimensions

Hold

SparseArray

{900, 900}

I'd say the place where I actually use Hold the most is when doing crazy things with like Block to ensure no evaluation, though, which often look like:

doASymbolicThing~SetAttributes~HoldAll
$defaultSymbolList = Thread@Hold[{x, y, z, a, b, c}];
doASymbolicThing[symbolicExpr_, 
  symbols1 : {__Symbol}, 
  symbol2_Symbol
  ] :=
 Replace[
  Thread[
   DeleteDuplicates@
    Join[Thread[Hold[symbols1]], $defaultSymbolList, {Hold[symbol2]}],
   Hold
   ],
  Hold[symList : {__Symbol}] :>
   Block[symList,
    processSymbolicExprSafely[symbolicExpr, symList]
    ]
  ]
$\endgroup$
8
  • 1
    $\begingroup$ How would amIDifferentNow be different from amIDifferentNowHeld? ValidQ does not have any hold attributes, so data would evaluate anyway before ValidQ sees it, right? Shouldn't you use ValidQ[Unevaluated[data]] in amIDifferentNowHeld? $\endgroup$ Oct 25 '19 at 13:59
  • $\begingroup$ @SjoerdSmit This is true. The effect will be the same (in that there is no effect) but you're right that if I expected a difference that wouldn't have picked it up. $\endgroup$
    – b3m2a1
    Oct 25 '19 at 14:00
  • $\begingroup$ With these new definitions, amIDifferentNowHeld[Hold[myBigData]] returns True for me. $\endgroup$ Oct 25 '19 at 14:02
  • 1
    $\begingroup$ @SjoerdSmit ah right because it's testing the raw Symbol myBigData...I think the former definition was what I wanted because it's saying "Does Hold do anything?" and the answer to that is "No it doesn't touch the underlying data and 're-evaluating' that data also does nothing" $\endgroup$
    – b3m2a1
    Oct 25 '19 at 14:03
  • $\begingroup$ Aren't you assuming that copying would strip 'valid' flag? If so, is it a safe assumption? $\endgroup$
    – Kuba
    Oct 29 '19 at 6:52

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.