7
$\begingroup$

I really love the flexibility of Mathematica: there are several ways to perform one task. However, to get the performance of the intense numeric calculation, it can cause some confuses. I wonder is it the real strength or the weakness of the language.

Example: Take a list of the first element in a matrix.

test1 = Transpose[{Range[10^8], Range[10^8]}];

The input list is Packed Array.

Developer`PackedArrayQ[test1]
True

For this simple task, there are many ways to do that. Now guess the performance of these commands:

(* test1 /. {a_, _} -> a; // Timing *) (* WARNING: May lock up your Mathematica! *)
First /@ test1; // Timing
test1[[All, 1]]; // Timing
Transpose[test1][[1]]; // Timing
First[Transpose[test1]]; // Timing
Take[Transpose[test1], 1]; // Timing

I think that, "Oh, the third one which uses only one function Part. This one should be the fastest". The rule of thumb, is:

  • Use lesser function will improve the speed
  • Treat the data as the whole
  • Use built-in function
  • Use packed array, etc
  • Avoid using Patterns for numerical calculation

So test1[[All, 1]] should be the fastest. But no, I'm wrong.


Timing results:

The slowest solution is:

test1 /. {a_, _} -> a; // Timing

Don't run this, because Mathematica will be stuck. (I need to Abort the Evaluation). It's obvious because Pattern searching and replacement are expensive. Luckily I didn't often use this type of programming.

The next slow solution is:

First /@ test1; // Timing

{2.90625, Null}

Surprisingly, Part is the next slow solution. I wonder why? This is the only case that uses one function Part.

test1[[All, 1]]; // Timing
{1.21875, Null}

And the combinations of 2 functions approaches are faster. Transpose and then Part, First and Transpose, Take and Transpose.

Transpose[test1][[1]]; // Timing
First[Transpose[test1]]; // Timing
Take[Transpose[test1], 1]; // Timing


{0.765625, Null}

{0.734375, Null}

{0.609375, Null}

The main question here is, there are too many approaches to perform the same operation. And normally, I didn't know which approach is the most optimal way in terms of efficiency.

$\endgroup$
  • 5
    $\begingroup$ It can depend on the system, but AbsoluteTiming or RepeatedTiming may be better tools to measure performance than Timing. $\endgroup$ – Michael E2 Oct 29 at 0:32
  • 1
    $\begingroup$ I'm not sure what the real question is here. The behaviors exhibited are exactly what one would expect. Perhaps try your experiment using non-square matrices for further illumination. $\endgroup$ – ciao Oct 29 at 1:01
  • 4
    $\begingroup$ @ciao I don't share your opinion that this is expected. Personally, I'd expect Part to have the best performance here because it's the idiomatic way to do things and I'd expect internal optimizations to be in place for this. $\endgroup$ – Sjoerd Smit Oct 29 at 8:09
9
$\begingroup$

The main question here is, there are too many approaches to perform the same operation. And normally, I didn't know which approach is the most optimal way in terms of efficiency.

Mathematica's performance is hard to predict, even more so than that of other high-level languages. There is no simple guideline you can follow. There will always be surprises and the behaviour will change from one version to the next.


Some insight into why Transpose is faster here:

On my machine (macOS / M12.1) Timing reports the lowest numbers for Part, not for Transpose. However, RepeatedTiming (which is based on AbsoluteTiming) reports a lower number for Transpose.

In[16]:= test1[[All, 1]]; // Timing
Out[16]= {1.32521, Null}

In[17]:= test1[[All, 1]]; // RepeatedTiming
Out[17]= {1.41, Null}

In[18]:= First[Transpose[test1]]; // Timing
Out[18]= {2.08334, Null}

In[19]:= First[Transpose[test1]]; // RepeatedTiming
Out[19]= {0.80, Null}

Typically, this is an indication that some operations are done in parallel. Timing measures the total time spent by each CPU core, while AbsoluteTiming measures wall time.

A quick look at the CPU monitor confirms that indeed, Part is single threaded (I see 100%) while Transpose is multi-threaded (I see ~250%).

This explains the difference.

| improve this answer | |
$\endgroup$
  • $\begingroup$ Oh. Thank you. I forget about the multi-threaded too. $\endgroup$ – Nam Nguyen Oct 29 at 13:08
  • $\begingroup$ Whether Part should or shouldn't be multithreaded—that's debatable. I disabled multithreading and benchmarked this again, and on my machine at least, and with this specific size of array, Transpose is still very slightly faster than Part. $\endgroup$ – Szabolcs Oct 29 at 14:34
  • 1
    $\begingroup$ @NamNguyen I don't know the state of the art of Transpose at Wolfram, but transposing tensors is a highly researched and important operation. Solutions exist that take account of the available cores and cache. In a large-memory problem, memory management is likely to be a major factor. On an Intel machine, Transpose probably calls the MKL routine mkl_?imatcopy. As Szabolcs said, though, these things change over time $\endgroup$ – Michael E2 Oct 29 at 15:08
  • $\begingroup$ Thank you very much @MichaelE2. $\endgroup$ – Nam Nguyen Oct 29 at 15:32
  • $\begingroup$ I also prefer using Range over Table. The only reason I use Table is for this: Table[x, {4}], because I didn't know another function to generate a list of Symbol like this. $\endgroup$ – Nam Nguyen Oct 31 at 22:25
3
$\begingroup$

This is another observation, that sometimes in Mathematica, combining 2 functions is faster than using 1 function.

Jon McLoone " 10 Tips for Writing Fast Mathematica Code " has proposed that "Using fewer function will speed up". But not all the case, I think.

Do a simple test: Using a function inside a Table to generate list.

In[11]:= a1 = Table[Power[i, 2], {i, 10^7}]; // AbsoluteTiming

Out[11]= {0.238681, Null}

Using Range first, and then put it in a functions .

In[12]:= a2 = Power[Range[10^7], 2]; // AbsoluteTiming

Out[12]= {0.0703124, Null}

Both are PackedArray.

In[16]:= Developer`PackedArrayQ /@ {a1, a2}

Out[16]= {True, True}

Maybe, Part, and Table are the big function? So they need to check something before doing the computational code? And Range, and Transpose is faster, because they are just doing one simple thing with less overhead?

Conclusions

  • Don't use Table[f,{i,iMax}]
  • But use f[Range[iMax]]

here is the performance proof:

testTable[n_] := AbsoluteTiming[Table[Power[i, 2], {i, 10^n}];]
testRange[n_] := AbsoluteTiming[Power[Range[10^n]];]

nList = {4, 5, 6, 7, 8};

t1 = First@testTable[#] & /@ nList;
t2 = First@testRange[#] & /@ nList;

ListLinePlot[{Transpose[{nList, t1}], Transpose[{nList, t2}]}, 
 PlotLegends -> {"Table", "Range"}, Mesh -> All]

enter image description here

| improve this answer | |
$\endgroup$
  • $\begingroup$ Yes, basically use vectorization whenever you can. $\endgroup$ – Szabolcs Nov 1 at 17:49

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.