How To: Optimizing Code
Posted: (EET/GMT+2)
How To: Optimizing Code
Level: All
In the past, optimizing program code was a integral part of program
development. Today, optimizing two clock cycles from a function doesn't
seem very impressive any more. Still, advanced programmers know how
the compiler and the computer itself works, and can be sure that their
code is optimized to the maximum extent possible.
Of course, optimizing code is a very difficult task. Usually people tend to think that optimizing is only done to get the best performance. In the past, speed was a major question. But when memory was valuable, code size was also the target of optimization. Also, code can be optimized for maintainability and development time. While this How To can only scratch this difficult and interesting subject, the following tips should help you write better programs.
Before we begin, few words still. Because every possible optimization cannot be explained here, nor can I say when this or that optimization should be used, using your own common sense is required. Note that trivial optimizations such as avoiding floating point operations are not listed here. Please also note that even highly optimized code cannot be fast if the algorithm is bad. A word of wisdom: "study thy algorithms."
Exception handling
Lets start with exception handling. In normal applications, exception handling is used often to protect resources and inform the user or end the application gracefully if an exception occurs. But did you know that exception handling is slow? When a exception occurs, the operating system and application code must execute hundreds of instructions to handle the exception. Assume the following code:
Function GetSquare(P : PInteger) : Integer;
Begin
Try
Result := Sqr(P^);
Except
ShowMessage('P was nil!');
Result := -1;
End;
End;
While the previous optimization was purely a performance optimization,
strings can also be used to optimize for code size. For example,
assume that you set menu hints to display why an option is
unavailable:
Another less-known feature in Delphi is RTTI or run-time type
information. Sure, the "as" and "is" operators are documented and
widely used, but did you know that even enumerated type value names
appear inside the EXE? For example, before I figured this out, I
happily wrote code like this:
Another thing that distinguishes a good programmer from the excellent
is the way he or she uses compiler directives. Usually these
directives are controlled through the Delphi IDE, but they can also be
set in code. In the final version of the application, the programmer
knows to disable the directives $R, $S, $Q and $B (if possible). Also,
the directive $O (optimization) should be on, of course. And if the
programmers really knows what she does, she won't forget the $A, $U, $Z
and $IMAGEBASE directives.
If P is nil, an exception occurs. Many clock cycles must be wasted
before the message can be displayed. In this kind of simple situations
it would be more efficient to first test if P actually was nil with an
"if" statement:
Function GetSquare(P : PInteger) : Integer;
Begin
If (P = nil) Then Begin
ShowMessage('P was nil!');
Result := -1;
End
Else Result := Sqr(P^);
End;
Exception handling also has another caveat the estimable programmer
knows and avoids. A performance degrade occurs if the standard
Exit procedure is used within try..finally blocks. This is because
calling Exit results in a so called "abnormal termination" of the block.
Thus code like this:
Var P : PChar;
Begin
GetMem(P,1024);
Try
...
If (P^ = 'Ouch') Then Exit;
...
Finally
FreeMem(P,1024);
End;
End;
Would be more efficient if written like this:
Var P : PChar;
Begin
GetMem(P,1024);
Try
...
If (P^ <> 'Ouch') Then Begin
...
End;
Finally
FreeMem(P,1024);
End;
End;
Strings
Almost all applications use strings. In Delphi 2, the "normal" strings
are long and can avoid the 255 character limitation found in Delphi 1.
These dynamically allocated strings are - among other things -
reference counted and thus usually quite fast. Still, strings can be
used unefficiently. Most common "error" is to assume that long strings
initially contain a garbage value. Assume the following:
Procedure GreatestOnEarth;
Var S : String; { a long string, not short! }
Begin
S := '';
...
End;
The assignment to clear S is totally unnecessary. This applies to all
long string variables, both global and local (but not to function
Result strings!).
Procedure SetDisabledHints;
Begin
FileSave1.Hint := 'This item is disabled because no files are open.';
FilePrint1.Hint := 'This item is disabled because there is no selection.';
EditPaste1.Hint := 'This item is disabled because clipboard is empty.';
...
End;
Above, the string "This item is disabled because" is repeated. If you
look the resulting EXE file, you can see that the string is repeated,
even if it is unnecessary. Lets try again with a simple constant:
Procedure SetDisabledHints;
Const Because = 'This item is disabled because ';
Begin
FileSave1.Hint := Because+'no files are open.';
FilePrint1.Hint := Because+'there is no selection.';
EditPaste1.Hint := Because+'clipboard is empty.';
...
End;
This won't help a bit because constants are evaluated at compile
time. But sometimes small things can make a big difference:
Const Because : String = 'This item is disabled because ';
Above, making Because a typed constant will make the string only
allocated once, and the concatenation will be made at run time. Of
course, this is slower than the two other methods, but if you have
dozens of this kind of assignments, code size can be a lot smaller.
Optimizer, compiler directives and RTTI
Many people know that Delphi has an optimizer which is very good. It
optimizes code by using CPU registers instead of memory variables, and
eliminates common subexpressions without requiring special awareness
from the programmer. Still, the optimizer is one of the least-known
features in Delphi. For example, assuming that the compiler can
eliminate all common subexpressions can lead to inefficient code.
Assume a normal procedure and the function it uses:
Function ThirdPower(X : Integer) : Integer;
Begin
Result := X*X*X;
End;
Procedure ShowValue(X : Integer);
Begin
If (ThirdPower(X) > 10000) Then ShowMessage('Huge')
Else If (ThirdPower(X) > 1000) Then ShowMessage('Large')
Else If (ThirdPower(X) > 100) Then ShowMessage('Small')
Else ShowMessage('Tiny');
End;
One might think that the compiler would optimize the ThirdPower calls
(except the first of course) away because they are called with the
same parameter. This is not true, because functions can have side
effects. For example, the ThirdPower could increment a simple counter,
and if the call would be optimized away, the counter would not be
incremented correctly. Because of this, Delphi must call the function
three times, if X would be smaller than five. A more optimized version
would be:
Function ThirdPower(X : Integer) : Integer;
Begin
Result := X*X*X;
End;
Procedure ShowValue(X : Integer);
Var I : Integer;
Begin
I := ThirdPower(X);
If (I > 10000) Then ShowMessage('Huge')
Else If (I > 1000) Then ShowMessage('Large')
Else If (I > 100) Then ShowMessage('Small')
Else ShowMessage('Tiny');
End;
When the optimization is on, the compiler could optimize I to a CPU
register, and as a result, the code would execute faster. Generally
speaking, optimization should be always on, but you should not trust
it blindly.
Uses MPlayer;
...
Function MediaPlayerButtonName(Button : TMPBtnType) : String;
Begin
Case Button of
btPlay : Result := 'btPlay';
btPause : Result := 'btPause';
...
End;
End;
Such a waste of coding time and EXE file size! You might wonder how
this wastes EXE size. This is simply because the button names already
are there! A optimized version using RTTI:
Uses MPlayer, TypInfo;
...
Function MediaPlayerButtonName(Button : TMPBtnType) : String;
Begin
Result := GetEnumName(TypeInfo(TMPBtnType),Ord(Button));
End;
Ain't it beautiful? It might not be as fast, but it surely looks
efficient.
Other tips
Above, three categories of optimization are explained hopefully with
required detail. Below, there is a list of miscellaneous tips with
a short explanation.
This can lead to huge code size savings if the same (large) bitmap
is used on many forms. This is because the same bitmap is saved along
with the form. If the bitmap is on multiple forms, the bitmap will be
saved multiple times.
Although Delphi's optimizer is good, it can (hopefully) never
match hand tuned assembler. Meet the challenge and just do it!
As with the optimizer, bare Windows API calls are always
faster than component based calls. But beware: for example, TCanvas
caches its resources so that if you don't know what you're doing,
resulting code can be slower.
Once again, if you avoid components and code the things directly,
you can save few kilobytes of EXE size and some milliseconds too. Be
careful though: this only optimizes code size and speed, not coding time
itself!
I hope that these tips help you write more efficient programs faster.
Still, there are hundreds - if not thousands - of other tips, and
surely many books could be written of them. But if you are interested in
code optimization (for speed), you should look at profiling tools, or
profilers for short. There ain't many available for Delphi, but if I
may advertise here, I'm just working on one!