4
$\begingroup$

I'm writing a lot of print functions for different parts in a package and at the moment am using something like

myprint[ identifierString_String ][ data_ ] := ...

where the identifierString is some string that determines what data will be printed (and reminds me where I use that specific function).

A lot of the strings are of the form

"functionName_subpart_subsubpart"

If I have a huge amount of different strings for which the function myprint is defined, would it be faster to use a definition as follows:

myprint[functionName_String][subpart_String][subsubpart_String][data_]

or does Mathematica use some built-in hashing functionality so that it doesn't have to loop over the whole list of { myprint[string1], myprint[string2], ... } to find the correct match?

I wrote the following code to check which one would be faster and the timing for the code where the strings are split is worse than the code with full strings. One thing that bothers me, though, is that in this timing the functions that use split strings need to access a list of strings and this might add a significant amount of time. I wouldn't know how to get rid of this extra time though...

(* Generate 280 strings from some basic building blocks. *)
tuples = Tuples[ 
  {
    { "FindZeroValues_", "SolveMonomialSystem_", "SolveFullSystem_", 
     "BreakSymmetry_", "CheckSolutions_", "RevertVariables", 
     "DeleteEquivalentSolutions" },
    {"ProperSystem_", "NoVars_", "EmptyList_", "NumericAnalysis_", 
     "Substitution_"},
    {"True", "False", "Invalid", "Trivial", "Ambiguous", "Error", 
     "Warning", "Failed"}
  } 
];

fullStrings = StringJoin @@@ tuples;

(* Define a print function using the full string as an argument *)
Do[
  myPrint1[string][data_] := data,
  {string, fullStrings } 
];

(* Define a print function using sub-parts of strings as arguments *)
Do[
  myPrint2[tp[[1]]][tp[[2]]][tp[[3]]][data_] := data,
  {tp, tuples}
];

RepeatedTiming[ 
  Table[ myPrint1[ string ][ 1 ] , {string, fullStrings} ];
] // First

(* Out: 0.00301837 *)

RepeatedTiming[
  Table[ myPrint2[ tp[[1]] ][ tp[[2]] ][ tp[[3]] ][ 1 ], {tp, tuples} ]
] // First
    
(* Out: 0.0119834 *)
$\endgroup$
2
  • $\begingroup$ Did you set strings=fullStrings somewhere? $\endgroup$ Commented Jan 5, 2022 at 0:25
  • $\begingroup$ Ow sorry, it should have been fullstrings. I used strings in my own test but thought the word fullStrings would be clearer so changed it manually. Post is edited now. $\endgroup$
    – Gert
    Commented Jan 12, 2022 at 10:26

1 Answer 1

7
$\begingroup$

Neither is really optimal. To get fast hasing it pays to have strictly pattern-free down values. We can get that by decoupling the patter part from the string part and using pure functions in the down values to handle the pattern part.

We'll create an experiment that we can adjust for size.

randStr[j_, k_] := Map[ToString, RandomInteger[{0, 10^8}, {j, k}], {2}]

tuples = Tuples[randStr[4, 7]];
fullStrings = StringJoin @@@ tuples;
Length[fullStrings]

(* Out[137]= 2401 *)

Clear[myPrint1]
Timing[Do[myPrint1[string][data_] := data, {string, fullStrings}]][[1]]
Timing[tab1 = Table[myPrint1[string][1], {string, fullStrings}];][[1]]

(* Out[141]= 0.767601

Out[142]= 0.362846 *)

Now create the down values is a way that is pattern-free.

Clear[myPrintFast]
Timing[Do[
   myPrintFast[string] = Function[data, data], {string, 
    fullStrings}]][[1]]
Timing[tab2 = 
    Table[myPrintFast[string][1], {string, fullStrings}];][[1]]
tab1 === tab2

(* Out[147]= 0.002781

Out[148]= 0.002235

Out[149]= True *)

Now multiply size by (a bit more than) 4.

tuples = Tuples[randStr[4, 10]];
fullStrings = StringJoin @@@ tuples;
Length[fullStrings]

(* Out[164]= 10000 *)

Clear[myPrint1]
Timing[Do[myPrint1[string][data_] := data, {string, fullStrings}]][[1]]
Timing[tab1 = Table[myPrint1[string][1], {string, fullStrings}];][[1]]

(* Out[166]= 14.5274

Out[167]= 7.22091 *)

Clear[myPrintFast]
Timing[Do[
   myPrintFast[string] = Function[data, data], {string, 
    fullStrings}]][[1]]
Timing[tab2 = 
    Table[myPrintFast[string][1], {string, fullStrings}];][[1]]
tab1 === tab2

(* Out[169]= 0.015552

Out[170]= 0.011183

Out[171]= True *)

The first case appears to be pretty much quadratic behavior, the second a bit worse than linear.

$\endgroup$
2
  • $\begingroup$ And would there be any difference between the case where the strings are split in substrings (so the function takes multiple arguments) and just using full string names? $\endgroup$
    – Gert
    Commented Jan 5, 2022 at 12:11
  • $\begingroup$ I have not tried but I would guess having multiple substring arguments might be slightly slower. More calls to hash functions, at a minimum. Also I do not know offhand how DownValues are done for multiple pattern-free arguments. $\endgroup$ Commented Jan 5, 2022 at 14:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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