15

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.}

Kuba
  • 136,707
  • 13
  • 279
  • 740
  • Good question. But I'm not entirely sure that your test is too meaningful... If I increase the size of data, I don't see any change in the timings. I've also tried to unpack the array and to use symbols with downvalues, without any clear influence on the result. – Lukas Lang Apr 10 '19 at 15:12
  • For your information, Mathematica does not use the concepts of pass by value versus pass by reference. See question 156319 – Somos Apr 10 '19 at 15:22
  • Isn't this a case of Function simply being well-optimized for this sort of thing? Also, you can get a bit of a speedup if you use Slot instead (Function[Null, #[[1]], HoldAllComplete]) – Sjoerd Smit Apr 10 '19 at 15:22
  • 1
    @LukasLang I appreciate better examples, this was the simplest that came to my mind. – Kuba Apr 10 '19 at 15:43
  • 2
    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. – Sjoerd Smit Apr 10 '19 at 15:44
  • 1
    @Somos I am not sure what concept it uses but if you write your code in a sloppy way 'copying' will make it slower. – Kuba Apr 10 '19 at 15:44
  • @SjoerdSmit I was able to reproduce this confusion with DownValues as well but Function was quicker to show. – Kuba Apr 10 '19 at 15:45
  • 1
    The documentation for HoldAllComplete[] has an example using the Hofstadter-Conway sequence which shows why it is sometimes faster to use it. – Somos Apr 10 '19 at 15:50
  • I thought Mathematica tried to be clever about only copying on modifications...but I guess I am not certain of that. Obviously one placeHold* will help a lot regardless is if you do lots of manipulations. Think passing a huge area through OptionsPattern and Merge-ing the results and things. There passing in a Hold[localVar$nn] has in the past bought me huge speed ups. – b3m2a1 Apr 13 '19 at 06:29
  • @b3m2a1 gimme best practices applicable to general audience :) Anyway, how isn't this question 'hot'. Why isn't there a penalty in hold vs non hold? – Kuba Apr 13 '19 at 06:32
  • @Kuba if I can think up some I will – b3m2a1 Apr 13 '19 at 06:33
  • 4
    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. – Henrik Schumacher Oct 19 '19 at 12:32
  • @HenrikSchumacher Those are important notes and I think the one about modification of data is counter intuitive. Your comment is along of what I am trying to achieve here. – Kuba Oct 21 '19 at 07:06
  • 1
    I think the distinction that you are expecting will only be apparent in the presence of computationally expensive delayed definitions. For instance, the second example but with data = ... changed to data := .... For immediate definitions with inert values, the computational time of the extra evaluations is vanishing small. – WReach Oct 25 '19 at 15:41
  • @WReach thanks, though I think it is an objectively intuitive behavior. I think my question boils down to not understanding well the concept of an 'inert expression' you mentioned and which appears sparsely in SE answers. – Kuba Oct 25 '19 at 15:54
  • @WReach now I even vaguely recall a topic where someone was discussing how expressions internally keep information about being evaluated' already during current evaluation''. – Kuba Oct 25 '19 at 16:05

2 Answers2

7

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.

Carl Woll
  • 130,679
  • 6
  • 243
  • 355
5

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]
    ]
  ]
b3m2a1
  • 46,870
  • 3
  • 92
  • 239
  • 1
    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? – Sjoerd Smit Oct 25 '19 at 13:59
  • @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. – b3m2a1 Oct 25 '19 at 14:00
  • With these new definitions, amIDifferentNowHeld[Hold[myBigData]] returns True for me. – Sjoerd Smit Oct 25 '19 at 14:02
  • 1
    @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" – b3m2a1 Oct 25 '19 at 14:03
  • Aren't you assuming that copying would strip 'valid' flag? If so, is it a safe assumption? – Kuba Oct 29 '19 at 06:52
  • @Kuba yeah but I believe I have seen that in the past. These flags are pretty much Expression specific I think. – b3m2a1 Oct 29 '19 at 06:54
  • @b3m2a1 maybe, maybe not :) I think we need a better statement than an assumption about an undocumented feature :) – Kuba Oct 29 '19 at 07:02
  • @Kuba you can test it with ExpressionStore. Use the same object as a key both before and after sending it through a function. That works by raw object identity, so that will tell you if the pointer has changed at all or not. In that case I can tell you there is absolutely no change as I make use of this in my code and so does, e.g. Jason Biggs – b3m2a1 Oct 29 '19 at 07:04