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;
    


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!).

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:

    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.

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:

    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.

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.


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.
  • Set TImage bitmaps in code.

      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.

  • Use assembler, if you know how to code down in the bare metal.

      Although Delphi's optimizer is good, it can (hopefully) never match hand tuned assembler. Meet the challenge and just do it!

  • Use Windows API

      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.

  • Avoid components

      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!



Questions? Comments? Give feedback. Or just fill in a form.