Monthly Archives: February 2006

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 writes:

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.