Quick optimization tip

| | Comments (23)

One thing I see a lot of, is code like this:
[rbcode]
for i as Integer = 0 to SomeFunctionCall - 1
. . .
next i[/rbcode]

While this code is clear and concise, it also could be hiding a very simple optimization. You see, the "SomeFunctionCall" is being evaluated on every pass thru the For loop. If it's a slow call, then this loop will slow down a bunch. So if you know that SomeFunctionCall is always going to return the same value, then you should pull it out of the looping construct to save time. So you'd do this instead:[rbcode]
dim count as Integer = SomeFunctionCall
for i as Integer = 0 to count - 1
. . .
next i[/rbcode]
By pulling it out, it's only evaluated one time.

Here's a real-world example of when this is the case. Let's say you want to loop over all the files in a folder (and you do not care if the folder contents are changing). Instead of putting someFolderItem.Count in there, you should pull it out of the loop. Why? Because not every OS has a quick function to determine how many files there are in a folder. On Windows, there's no Count function for a directory, so the framework has to loop over every single file and count them. Can you imagine how slow this API is for a directory of 15,000 files? In a loop? Being called 15,000 times? As you can imagine, you get a vast speed-up from your code when you can pull things out of the loop evaluation.

This trick is usually called "removing loop invariants" btw. Since the count is an invariant (it doesn't change), you can pull its evaluation out of the loop and just use a variable instead of a method.

23 Comments

Didn't know the terminology, but a good practice to get used to in performance-concious applications!

Would it not be even more optimal to pull the -1 out of the loop too? ::)

Anyway, an alternative would be "for i = (someFunctionCall-1) downto 0"--presuming, of course, that the order isn't critical.

It kinda seems like it'd make sense to have the destination value evaluated only once anyway. I mean, when the syntax is "for (int i = 0; i

However, why doesn't the compiler optimize this case itself?

Do you think it's worthwhile to pull out of the loop "calls" to a property? For example, Dictionary.Count, or Listbox.Count, etc..

To echo "Anonymous", if you want to keep the loop iterating in the same direction, change it to:

dim count as Integer = SomeFunctionCall - 1
for i as Integer = 0 to count
. . .
next i

Otherwise, the -1 calculation will be made on each iteration of the loop.

To Russ, yes -- if the property will not change, i.e., it is invariant while in the loop, pull it out of the loop to avoid making the method call on each iteration.

@Anon -- it would be, but it's one cycle, which really doesn't matter. Then it's an issue of clarity.

@Steve and John -- because then it's incorrect. You can't tell if it's invariant (only the programmer knows). For instance, let's say you're deleting files in the loop and *need* count to update. Then you want Count as part of the loop structure instead of outside of it; because it changes.

@Russ -- I'm not certain I understand what you're after.

@Aaron, I was trying to get a feel for whether it was worthwhile to store a Dictionary.Count or Listbox.Count property in a local variable before iterating through a loop.

I infer (based on Mr. Scott's answer) that Listbox.Count (etc..) incurs some overhead that just looking at the value of a local variable does not.

Obviously, though, if the Listbox.count might change during the life of the loop, then storing Listbox.count prior to the loop would be a bad thing!

Thanks!

@Russ -- it certainly doesn't hurt to pull those out if they're invariant. Just keep in mind that some calls will have more of an optimization than others. ListBox.Count is just a property access under the hood, so it doesn't take many cycles. However, Val( Mid( ReplaceAll( Left( someStr, 25 ), "#", "!" ), 8, 1 ) ) takes a lot of cycles, so if it's invariant, yanking it out of the loop is a better deal.

So.... YMMV is what I'm saying.

Isn't

for i as Integer = 0 to count - 1

re-evaluated every time though?

I always do count as Integer = SomeFunctionCall-1 in the hope it saves as many cycles as possible...

Yes, the count - 1 is evaluated every time. But it's evaluation is negligible when compared to the overhead of calling a method which does a bunch of processing and then returns a result. We're talking the difference between 3-4 cycles (for evaluating count - 1) and 1000s of cycles (for evaluating a method call + its processing).

While it can make sense to perform this optimization, you should always be careful making it unnecessarily. If you mean "start at zero and increment until we reach the number of elements in a dictionary minus 1", then write that, not "set a variable to the number of elements in a dictionary minus 1, then increment from 0 to the value of some variable". Your code is easier to understand as your intent is clear. It may seem obvious in small examples, but in the real world these little obfuscations can cause hard-to-find problems. For instance, when someone else (or you in a year) comes along and starts adding code to that routine and manage to change the result of the call (say, add an item to the dictionary) after caching its count.

Obviously, this is a good optimization for certain cases, but for others it is a waste (ubound comes to mind). Although I have seen (and continue to see) claims of stunning speed improvements by caching the upper bound of an array, the reality is it has no measurable impact in a real app (i.e. one where any work is done in the loop). And when I say measurable, I mean with "Microseconds()", not perceived speed. So, use this optimization when it will help, but don't apply it just to save a cycle or two.

(And yes, I realize Aaron wasn't saying to always use it, or that ubound has an impact - his example was a perfect case for applying it. I just wanted to add the caveat)

Perhaps a new keyword could tell the compiler to only evaluate the expression once.

@Brady -- I agree, code clarity is usually the most important thing. However, when clear code causes you to have performance hits, it's time to optimize a bit. And this is one case where the optimization is very simple, and usually doesn't lead to harder to read code.

@Adam -- no reason to clutter the language up. If you have the presence of mind to use that compiler flag, it's just as easy to move the evaluation up a line.

Not a compiler flag per se, just a syntax in the language like:

for i as integer = 0 to count as integer = MyFunction

I almost always do a:
iMax = SomeCall - 1

For iLoop = 1 to iMax

That way I know that I'm looping between 2 numbers and not recalculating on every loop.

Uh, premature optimization is the root of all evil. Get it readable and right first, then speed it up if such work is needed. Is there any time profiler for REALbasic?

BTW, some languages define clearly that they do evaluate the upper bound only once, i.e. changing the upper bound after the loop has been entered has no effect. I wish RB (or Visual BASIC?) had been aware of code optimization in the beginning and had adopted this rule as well. But I can also see the point that Aaron makes - if the upper bound is indeed variable, then at least the current solution in RB make it "safer" to work as intended.
And, of course, there are compilers that are capable of figuring out when even a subroutine does not change its return value over the loops - but that requires a rather powerful compiler we're not getting to see ever with RB, I'd bet :) (only if it would output C++ code and let an advanced C++ compiler do the work)

An interesting variation of the problem we have here:

. sub foo (d as Dictionary)
. dim v as Variant
. for each v in d.Values
. ... do something with v
. next
. end sub

"d.Values" is supposed to return an array of Variants. Creating this array surely takes some active processing time as the dictionary probably does not store the values in a array.

So, I wondered if on every iteration the array would get fetched again. I asked on the RB betas list and the reply suggested that it would NOT get fetched repeatedly but only once. And my own tests showed the same.

While this is good in regards to performance, it also means that it works contrary to how the classic for-to loop works:

While the class for-to loop's number of iterations can be altered inside the loop, the for-each loop's iterations are fixed, even if the array gets altered in the loop.

And that's not good, either (because of the confusion as this difference is not obvious and lot likely to be expected).

Oops, I used the "." in front of the code lines to keep the indentations, but that didn't work. Seems like this blog software does not like structured coders :)

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.