Bug fixed in version 11.1
Functions like MemberQ
, FreeQ
, etc. no longer unpack. Yay!
This question is inspired by one of @whuber's answers
Consider the following code:
μ = RandomReal[{0, 1}, 100];
Σ = DiagonalMatrix[Exp[RandomReal[{0, 1}, 100]]];
AbsoluteTiming[
RandomVariate[MultinormalDistribution[μ, Σ], 400000];]
It runs in 3.5 seconds here.
Now let's parallelize it:
LaunchKernels[]
AbsoluteTiming[
Join @@ ParallelTable[
RandomVariate[MultinormalDistribution[μ, Σ], 200000], {2}];]
This runs in 6.3 seconds on a 2-core machine---much slower. It also uses a lot of memory (which I checked using a process monitor).
Now let's suppress returning the results from the subkernels by including a semicolon:
AbsoluteTiming[
Join @@ ParallelTable[
RandomVariate[MultinormalDistribution[μ, Σ], 200000];, {2}];]
This one runs in 2.6 seconds---a speedup.
What is happening here? Why is the calculation that returns the result so much slower? Is it a general rule with parallel calculations that returning even moderately large data tends to lead to a significant slowdown? Is the slowdown due to MathLink's performance? Is there anything one could do to avoid the slowdown?
Warning: This might eat all your memory and force your system to swap! This computer has 6 GB and everything was fine. If you have less memory, reduce the amount of data a bit.
Solution
@Oleksandr's excellent analysis showed that the performance bottleneck is MemberQ
, in particular that it unpacks all arrays inside the expression tested. This is completely unnecessary, and it's possible to define a more efficient (though more limited) version of MemberQ
:
memberQ[list_, form_] := Or @@ (MatchQ[#, form] & /@ list)
Note that MemberQ
only tests at level 1 by default (unlike FreeQ
which tests at all levels). This made it easy to re-implement the two-argument form of MemberQ
.
We can temporarily change MemberQ
while executing parallel operations:
ClearAll[fix]
SetAttributes[fix, HoldAll]
fix[expr_] := Block[{MemberQ = memberQ}, expr]
fix@AbsoluteTiming[
Join @@ ParallelTable[
RandomVariate[MultinormalDistribution[μ, Σ],
200000], {2}];]
This runs in 3.0 seconds now, a huge improvement.
This is just an illustration of how to fix the performance problem, but the code I showed here is not completely safe to use in its current form.
Some notes:
Changing builtins is always risky, and can easily cause problems
I used
Block
to localize the change, which reduces the risk. Note thatBlock
will not affect calculations in the parallel kernels, so iffix
is used only on the parallelization functions, in the formfix@ParallelTable[...]
, then it will only have an effect for these functions, but not for the code that is being parallelized. This reduces the risk further.I did not implement the 3-argument form of
MemberQ
. If this is used anywhere in the parallel tools,fix
will break things. It'd take a bit more work to correctly implement this too, preferably just falling back to the builtinMemberQ
for this case. There may always be some undocumented behaviour ofMemberQ
which we are not aware of and which differs frommemberQ
.I did not implement short circuiting, so
memberQ
will be slower in some cases. This can be fixed as well.
These potential problems can largely be fixed with a bit of work, and I believe this method can work well for fixing this particular performance problem of parallel calculations.
ParallelDo
is used instead ofParallelTable
the speedup is ~4 . So not 8. $\endgroup$