While Visual Basic will likely never attain the blinding performance of C++ or Delphi, it’s a respectable performer under normal circumstances. But there are exceptional circumstances when “respectable” just isn’t fast enough. In this article, I’ll give you five performance optimization tips you can implement in your own applications when speed is at a premium.
Warning: Obfuscated code ahead
Remember that generally, performance comes at the cost of readability. All those nice habits you’ve cultivated to write code that’s easily read and understood often exact a price in terms of speed (and memory usage, but that’s a different matter). Conversely, all the tips I’ll discuss here usually result in less readable code. So it’s important to weigh performance gains against obfuscation and to liberally comment your source when you optimize: Always leave some breadcrumbs for the next guy.
The grotesque Gosub
I’m sure that we’ve all been told that Gosub is evil and should be avoided at all costs. It’s much more acceptable from a readability and reuse standpoint to place subroutine code in its own Sub or Function. But remember that minilecture I gave you earlier about performance optimizations coming at the expense of readability? This is one example of that tradeoff: replacing a frequently called utility Sub or Function with a set of local subroutines, the kind you’d write using a Gosub, can gain you on the order of three times faster code execution.
Why? Calling a function is a relatively expensive process because VB must preserve the context (variable values, the current execution point, and some other things) of the calling code, push any arguments onto the stack, and locate the function’s entry point in memory before calling it. Local subroutines, on the other hand, have much lower overhead requirements: There’s no variable stack required.
You could, of course, achieve much the same result by writing whatever code you’ve placed in your utility functions or subs inline. However, if you’re saddled with a completed or mostly completed application that’s simply too slow, it’s usually easier to paste Gosub blocks into your code than to rewrite the block of code itself.
Use appropriate variable types
When writing code, make sure that the variable types you declare are appropriate for their intended use. For example, don’t declare a loop counter variable as a Variant; use a native numeric type instead. Similarly, when dealing with objects, you’ll want to declare the actual type of the object instead of generic Object variables where possible to take advantage of the performance gains provided by early binding.
Okay, I’ll admit that those were rather obvious. Did you know, though, that all numeric types are not created equal, and the differences can be rather counterintuitive? For instance, a For… Next loop controlled by a Byte (1 byte) variable will take slightly longer to complete than an equivalent loop controlled by an Integer (4 bytes).
Here we go loopty-lie
While we’re on the subject of For loops, when dealing with collections or other object types that support it, For Each… Next should be used instead of For… Next, as the former yields approximately a 33 percent gain in execution speed over the latter.
The reason for this is pretty simple. Using a For Each, VB needs to locate the address of the object variable only once, at the beginning of the loop. In a For… Next scenario, the object variable is dereferenced in the body of the loop on each iteration. Using a With statement where appropriate yields similar results.
Turn off GUI updates
Building GUIs with VB is so easy partly because the controls draw themselves for you, automatically updating their appearance based on property settings you make in VB. Unfortunately, when performing a long operation that updates a control, these well-intentioned widgets can come back to bite you when a control insists on redrawing itself after each update, slowing the process drastically.
Forms and picture controls have an AutoRedraw property you can use to turn off this behavior, forcing the control to hold off on drawing itself until you’re finished working with it. What to do with other controls that don’t expose an AutoRedraw property?
The Windows messaging API provides an answer. Using the SendMessage API function, you can send the WM_REDRAW (&HB) message to a control to determine its redraw behavior. Passing False or zero as the wParam argument turns off redrawing, and you’d pass True or any nonzero integer to turn redrawing back on. In addition to drastically speeding up a set of formatting operations on a control, turning off redrawing can make your application look much cleaner as well.
There is a message for you
For more information on the Windows messaging API and what it can do for your applications, check out “Tap into hidden VB control functionality using SendMessage.”
Advanced compiler optimizations
Starting with VB5, developers have had a bit more control over the VB compiler. You can choose to have your application compiled in the traditional p-code format, which is essentially interpreted at runtime, or compile it as true Win32 native code. Compiling your applications as native code provides you with a quick, safe, and easy speed optimization, around 20 times faster than p-code. However, you should be aware that a small handful of very esoteric bugs are associated with native code compilation.
Chances are, you’re already compiling to native code (and optimizing for speed), since these are the defaults in VB6’s Make Options dialog. There’s also an Advanced Optimizations button that provides you with a few more options (Figure A), which you can use to speed up your code under certain circumstances.
|VB6 advanced optimizations|
Note that because we’re dealing with compiler switches here, you will have to fully compile an application and run it outside the VB IDE to notice any speed benefits. Each of these optimizations causes the compiler to make some fundamentally different assumptions about your code and how you wrote it, removing certain checks and safeguards in the name of performance. In all cases, these switches change how compiled VB code behaves, so use them with due caution.
Let’s take a quick look at what each of these optimizations does:
- Assume No Aliasing tells the compiler that you will not use two different names for the same memory location in different parts of your application, meaning you will not be passing variables ByRef. If the compiler knows this, it can make various optimizations like storing variables and function arguments in registers instead of the stack, thereby increasing performance.
- Remove Array Bounds Checks prevents the compiler from checking that all array indexes you specify are valid for a given array. Every time your code indexes into an array, VB checks that the index you supplied is within the declared bounds of the array. If it isn’t, VB raises an error rather than lets you access a memory location outside of the array and potentially crash your application. By removing these array bounds checks, you are telling VB to not bother double-checking your array indexes and to instead assume that they are always correct. If your code does a lot of array manipulation, using this optimization can dramatically increase your application’s execution speed, but be sure to triple-check your array indexes.
- Remove Integer Overflow Checks tells the compiler not to check that values you assign to integer variable types (Byte, Integer, Long, and Currency) are within that type’s legal range. VB performs such a check every time an integer math operation is performed. Turning off these checks can speed up integer math operations, but results that fall outside the legal range will be incorrect, wrapping to the opposite end of the range. For example, adding 1 to an Integer that’s currently set to 32767 will result in -32766.
- Remove Floating Point Error Checks works similarly to the integer overflow check optimization above but applies to floating-point variable types (Single and Double) instead.
- Allow Unrounded Floating Point Operations lets the compiler skip rounding floating-point numbers to the same precision before comparing them, providing a modest increase in speed. This optimization can cause some exact equality comparisons between floating-point variables to produce unexpected results because of a difference in precision between the two variables.
- Remove Safe Pentium FDIV Checks removes additional code that the compiler adds to compensate for the floating-point division bug discovered in Pentium processors several years ago. Removing the safe Pentium code will generally speed up floating-point division operations, but of course if your code runs on a machine that’s affected by the division bug, the results of an operation may not be correct. I wouldn’t recommend this optimization unless you’re absolutely positive that your code will never run on a buggy CPU (an AMD-only shop perhaps).
Always look to the algorithm
The tips I’ve provided here can be useful to help you wring the most performance out of an application. However, it’s usually best to keep your eye on performance throughout the process, choosing sound algorithms and design strategies to help you on your way. To coin a phrase, an ounce of planning is worth a pound of tuning.
Share your tips
If you have a performance tip that you’d like to share, send us an e-mail with your suggestion or post a comment below.