Common mistake: Don’t rely on exception handling.

Steve Trefethen, Senior R&D Engineer Delphi/C#Builder for the Borland Software Corporation had an EXCELLENT post on his BLog. It emphasizes the cost of a mistake that I always see in example code on the web and in WordPress plug-ins as far as that goes.

Here’s the law that is always broken: “If you know an error is likely to occur in your code, check for that condition first.  DO NOT rely on exception handling detect it.”

This applies whether you are waiting for file access to fail  or an SQL insert statement to come back with an error.  Check to see if the condtion (or record) exists and then have your program handle it correctly.

Generic exception handling should only be used when you cannot predict or test for the condition. 

Newer versions (5+) of MySQL have a clause for inserts called ON DUPLICATE KEY.  Many MySQL programmers are starting to rely on it, but it too has a cost.  Even a single call that runs two insert queries with exception handling thrown in there is almost always going to be slower than checking for the record first and chosing what to do based upon the result of the read only select.  The ONLY reason I see for that call to exist is if it locks the database as it is running and therefore you KNOW the has not changed after your Select query is run. So it does have its application.

 Maybe this article will help others come around: 

BDS 2006, slow compile times and the cost of exceptions

While browsing the newsgroups I ran into a thread where a user had commented that BDS 2006 compiled his application incredibly slowly. A while ago I’d had a discussion with Mark Edington our resident performance maverick about this issue and it turns out that it’s a good reminder of the cost of raising an exception or in this case lots of them.

Since Mark debugged this issue he kindly provided me with a short write up of the specifc problem, it’s solution and a workaround.

Mark write:
In BDS 2006 in Delphi a performance issue arises when compiling projects in the IDE when the source files in the project are marked as read-only on disk. The more files in the project the more significant the delay. The problem only affects projects that have read-only source files. The root of the problem, is a callback procedure which is called by the compiler when compiling in the IDE. The callback is designed to open the file and return an IStream interface:

 


function OpenOSFile(const Name: string): IStream;
begin
  { try opening existing file so that read/write operations are
    possible }
  Result := nil;
  if FileExists(Name) then
  begin
    try
      Result := TOSFileStream.Create(Name, fmOpenReadWrite or 
        fmShareDenyNone);
    except
      on EFOpenError do
        Result := TOSFileStream.Create(Name, fmOpenRead or 
          fmShareDenyWrite);
    end;
  end;
end;

Notice the try/except block, since there is no parameter to the function that specifies what access rights to use when opening the file, the procedure tries to be accommodating and provide read/write access first and if that fails it resorts to to opening the file as read-only. Unfortunately, as written, it introduces a serious performance penalty for anything other than small projects. Interestingly enough, the performance issue isn’t from the fact that it takes 2 attempts to get a read-only file opened, it is actually caused by the exception that is raised when the first (read/write) TOSFileStream.Create call fails.

In the test case that was submitted for diagnosing this problem the RaiseException procedure (which is a Windows API call), was accounting for nearly 40% of the total compile time. Under a profiler it took nearly 50 seconds to execute that procedure about 1100 times. That works out to around 45 milliseconds per call which may not sound like that much, but when executed 1100+ times it adds up.

The solution was very straightforward: Rewrite the routine and remove the exception based fallback algorithm. The new version looks like this:


function OpenOSFile(const Name: string): IStream;
var
  Code: Integer;
begin
  Result := nil;
  Code := GetFileAttributes(PChar(Name));
  if (Code <> -1) and (FILE_ATTRIBUTE_DIRECTORY and Code = 0) then
  begin
    if (FILE_ATTRIBUTE_READONLY and Code = 0) then
      Result := TOSFileStream.Create(Name, fmOpenReadWrite or
fmShareDenyNone)
    else
      Result := TOSFileStream.Create(Name, fmOpenRead or
fmShareDenyWrite);
  end;
end;

This version first checks to make sure the file is not marked as read-only before attempting to open it as read/write. The FileExists call that was replaced in the original version of the procedure is actually implemented using GetFileAttributes anyway, so there was no extra overhead introduced to make the check.

The moral of the story is avoid writing code that raises exceptions as part of the normal course of program execution. Exceptions are horrifically expensive to raise and handle and should be reserved for the truly “exceptional” events. If you have a medium to large Delphi project that has files marked as read-only the workaround for this issue is to mark them as read/write until this fix is made publicly available.

posted on Monday, February 27, 2006 3:51 PM

Add a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.