+1 for Backwards Loops!
Latest activity Edit by John D'Errico
on 8 Sep 2024 at 14:47
Base case:
Suppose you need to do a computation many times. We are going to assume that this computation cannot be vectorized. The simplest case is to use a for loop:
number_of_elements = 1e6;
test_fcn = @(x) sqrt(x) / x;
tic
for i = 1:number_of_elements
x(i) = test_fcn(i);
end
t_forward = toc;
disp(t_forward + " seconds")
Preallocation:
This can easily be sped up by preallocating the variable that houses results:
tic
x = zeros(number_of_elements, 1);
for i = 1:number_of_elements
x(i) = test_fcn(i);
end
t_forward_prealloc = toc;
disp(t_forward_prealloc + " seconds")
In this example, preallocation speeds up the loop by a factor of about three to four (running in R2024a). Comment below if you get dramatically different results.
disp(sprintf("%.1f", t_forward / t_forward_prealloc))
Run it in reverse:
Is there a way to skip the explicit preallocation and still be fast? Indeed, there is.
clear x
tic
for i = number_of_elements:-1:1
x(i) = test_fcn(i);
end
t_backward = toc;
disp(t_backward + " seconds")
By running the loop backwards, the preallocation is implicitly performed during the first iteration and the loop runs in about the same time (within statistical noise):
disp(sprintf("%.2f", t_forward_prealloc / t_backward))
Do you get similar results when running this code? Let us know your thoughts in the comments below.
Beneficial side effect:
Have you ever had to use a for loop to delete elements from a vector? If so, keeping track of index offsets can be tricky, as deleting any element shifts all those that come after. By running the for loop in reverse, you don't need to worry about index offsets while deleting elements.
5 Comments
Cute. A sneaky preallocation that uses prestadigitation. It works of course because a statement like
x(10) = 1
creates a new vector of length 10, where only the last element has been assigned, and the very first iteration of the loop implicitly does exactly that. And at no point did my fingers leave my hand!
Using a reverse loop to delete however, is still not efficient, because that loop still performs dynamic reallocation of memory at each step. The code is nice and clean, and easy to read, so that is a good thing.
n = 1e5;
tic
y = 1:n;
for ind = n:-1:1
if mod(ind,2) == 0
y(ind) = [];
end
end
toc
tic
y2 = 1:n;
y2(mod(1:n,2) == 0) = [];
toc
So the loop is MUCH less efficient, because at each step MATLAB reallocates memory. And the longer the loop, the worse it gets. A forwards loop might look like this:
tic
y3 = 1:n;
deleteflag = false(1,n);
for ind = 1:n
if mod(ind,2) == 0
deleteflag(ind) = true;
end
end
y3(deleteflag) = [];
toc
So fairly efficient, but extra code, and I had to create a secondary logical vector on the side to flag elements for deletion. Note this last loop could have been written to run in either direction, because no deletion was done until the end.
For shorter loops where the time is not a factor, the backwards deletion loop is a beautiful thing, since the code is nice and clean (and it works!) when a sloppily written forwards deletion loop completely fails.
Overall, a nice idea, well worth keeping in your bag of tricks.
Wow, such a clever tip! Thanks for sharing.
After I originally posted this topic, someone pointed out that certain speedup claims (backwards vs. forwards) were unlikely, given statistical noise. Accordingly, I updated the end of the post to more accurately describe the situation.