arrayfun vs loops again
Show older comments
I realise that the speed of arrayfun versus for-loops has been discussed before, but I remain puzzled by why the difference is as huge as it is in certain circumstances, and I wonder whether this is seen as an issue by the community or by MathWorks.
First an example - my questions are at the end. Define a simple class
classdef testclass
properties
p
end
methods
function tc = testclass
tc.p = pi;
end
end
end
a function that extracts a property value from a vector of objects of the class using a loop
function pvec = loopTest(tcvec)
pvec = double(size(tcvec));
for i = 1:length(tcvec)
pvec(i) = tcvec(i).p;
end
end
and one that does the same using arrayfun.
function pvec = arrayfunTest(tcvec)
pvec = arrayfun(@(t) t.p, tcvec);
end
and then run some timing code:
tc = testclass;
tcvec = repmat(tc, 1, 1000);
tloop = timeit(@() loopTest(tcvec));
tarrayfun = timeit(@() arrayfunTest(tcvec));
fprintf("arrayfun takes %f times as long as a loop\n", tarrayfun/tloop);
which on my Windows PC running R2024a, prints (typically)
arrayfun takes 52.565968 times as long as a loop
A factor of 50! This effect accounts for some of the remarkably long run times that were plaguing my app.
One standard explanation of the difference is that arrayfun has to do an extra function call, so let's see if that's what is causing this, by putting an extra function call into the loop:
function pvec = loopFunTest(tcvec)
pvec = double(size(tcvec));
f = @(t) t.p;
for i = 1:length(tcvec)
pvec(i) = f(tcvec(i));
end
end
and repeating the timing exercise:
tc = testclass;
tcvec = repmat(tc, 1, 1000);
tloopfun = timeit(@() loopFunTest(tcvec));
tarrayfun = timeit(@() arrayfunTest(tcvec));
fprintf("arrayfun takes %f times as long as a loop with a function\n", tarrayfun/tloopfun);
which now prints typically
arrayfun takes 6.163437 times as long as a loop with a function
So yes, the extra function call does have an effect. But there's still a factor of 6 between the loop and arrayfun - that's still huge, and enough to make arrayfun more or less useless. This a pity - more than a pity, a serious nuisance - as I like to use it, and my code is littered with examples as it was a natural go-to before I discovered this problem.
I've tried to make my examples clear enough to illustrate the core of the problem. My two questions are these:
- Out of curiosity, how do you make arrayfun run 6 times more slowly than a loop? I realise, of course, that ultimately it is implemented as a loop, but that only means we might not expect it to be a lot faster. But even the most naive implementation should allow it to be about as fast as the loop - so what is going on to produce the spectacular slowdown?
- What are the factors that interact with arrayfun to make it slow? Without doing masses of tests, how can I know which parts of my code need to be rewritten as loops? A sense of when it's slow and when it isn't would be really useful.
6 Comments
I don't have an answer to either of your questions. I just wanted to point out two things:
1. double(size(tcvec)) does not pre-allocate pvec properly (which I imagine was the intent):
tc = testclass;
tcvec = repmat(tc, 1, 1e5);
pvec = double(size(tcvec))
tcvec = tc;
pvec = double(size(tcvec))
So pvec is 1x2, and the subsequent for loop is appending elements to it from the third iteration.
Instead, use something like pvec = zeros(size(tcvec)).
(Of course, with proper pre-allocation of pvec, I would expect the for loop function to run faster than before, further highlighting the relative slowness of arrayfun in this case.)
2. I realize you are presenting only an illustrative example, but I think it's worth pointing out that you can do pvec=[tcvec.p] to achieve the same result as the other functions, and faster than any of them.
tc = testclass;
T = table();
for N = 10.^(0:5)
tcvec = repmat(tc, 1, N);
thorzcat = timeit(@() horzcatTest(tcvec));
tloop = timeit(@() loopTest(tcvec));
tloopfun = timeit(@() loopFunTest(tcvec));
tarrayfun = timeit(@() arrayfunTest(tcvec));
T{end+1,:} = [N thorzcat tloop tloopfun tarrayfun];
end
T.Properties.VariableNames = ["N" "horzcat" "loop" "loop w/function" "arrayfun"]
function pvec = loopTest(tcvec)
% pvec = double(size(tcvec));
pvec = zeros(size(tcvec));
for i = 1:length(tcvec)
pvec(i) = tcvec(i).p;
end
end
function pvec = arrayfunTest(tcvec)
pvec = arrayfun(@(t) t.p, tcvec);
end
function pvec = loopFunTest(tcvec)
% pvec = double(size(tcvec));
pvec = zeros(size(tcvec));
f = @(t) t.p;
for i = 1:length(tcvec)
pvec(i) = f(tcvec(i));
end
end
function pvec = horzcatTest(tcvec)
pvec = [tcvec.p];
end
Dyuman Joshi
on 6 Apr 2024
"But even the most naive implementation should allow it to be about as fast as the loop "
No, that is not the case.
In general (on CPU, the case might be different for GPU version of arrayfun, which you could try using if you have the Parallel Computing Toolbox), arrayfun will be slower than a for loop with preallocated output as it has the overhead of calling the function in each iteration. The for loop also has opportunities for optimizations between statements that the arrayfun (or cellfun) would not have.
As for factors affecting the speed of arrayfun - a major affect could be due to the function being used i.e. in addition to the overhead of calling the function, there is also the overhead of executing the function. One of the commonly used functions in arrayfun with considerable overhead is find.
"Without doing masses of tests, how can I know which parts of my code need to be rewritten as loops? A sense of when it's slow and when it isn't would be really useful."
That is something that comes with experience. I, myself, learnt this the longer way - https://in.mathworks.com/matlabcentral/answers/1793315-to-extract-the-last-sub-string-from-strings#comment_2343625
As you can see in the thread attached above, I had (naively) used arrayfun with find and the resulting performance is not good at all.
Sometimes there are better methods than a for loop as well - horzcat in this case as @Voss has shown above and sscanf as @Stephen23 has shown in the example linked.
And as Stephen adviced, I have followed this forum regularly and I continue to learn from it, even from my own mistakes. It will take some practice. I would suggest to you follow the advice Stephen has given as well.
Hope this helps.
David Young
on 6 Apr 2024
Out of curiosity, how do you make arrayfun run 6 times more slowly than a loop? I realise, of course, that ultimately it is implemented as a loop, but that only means we might not expect it to be a lot faster.
That description, "ultimately it is implemented as a loop", is doing what I'd call some heavy lifting. Consider the handling of error cases. This first example works.
a = arrayfun(@(x) zeros(x), [1 1 1])
This next one doesn't. But it gives an informative error message when it fails, highlighting exactly which element of the array over which you're iterating caused the error. If you tried to do this in a "naive" for loop, the message wouldn't be the same as for arrayfun and (without some effort on the part of the author of the loop, which would slow down the loop a little) it wouldn't highlight which iteration failed in the error message.
a = arrayfun(@(x) zeros(x), [1 1 2])
That checking of the output of your function likely isn't expensive in an absolute sense, but it isn't free either. And if the operation you're performing is fast, that validation of the function output could be a relatively expensive part of the arrayfun call.
So arrayfun isn't "just a for loop." A naive implementation might be, but the MATLAB implementation isn't that naive.
David Young
on 8 Apr 2024
Dyuman Joshi
on 16 Apr 2024
"simply because that implementation would be just to execute the exact same loop inside arrayfun."
"The overhead factors you mention surely apply equally to the loop and arrayfun, "
That is not is what being done in the for loop though.
@Voss has provided an example of running a loop with a function handle and calling it in every iteration, and as you can see it is slower than the simple for loop.
"... , which you won't notice."
That is not true. There is always some overhead to calling a function, which as I mentioned depends on the function being called.
Accepted Answer
More Answers (0)
Categories
Find more on Parallel Computing Fundamentals in Help Center and File Exchange
Community Treasure Hunt
Find the treasures in MATLAB Central and discover how the community can help you!
Start Hunting!