Delphi 3,4,5 TCollection Performance Issues and Solutions

By: Clever Components

Abstract: Delphi 3,4,5 TCollection Performance Issues and Solutions

If you are using TCollection classes in your Delphi 3,4 or 5 applications than you will find this article quite interesting.

Firstly let us try fairly simple code:

procedure TForm1.Button1Click(Sender: TObject); 
var 
   old: TCollection; 
   i: integer; 
begin 
   old := TCollection.Create(TCollectionItem); 
   for i := 0 to 100000 do 
   begin 
      old.Add; 
   end; 
   Windows.beep(900, 1000); // hi-freq beep after we done with adding empty items 
   old.Free; 
   Windows.beep(100, 1000);// low-freq beep after we done with destroying empty items 
end;  

You might think that low-freq beep will follow right after hi-freq beep (well what can be faster that just simply destroy all collection items) - but it is not.

In fact it takes 10-20 seconds to destroy collection which hold few dozen thousands items - and worse of all your CPU will be 100% busy. We bumped into this problem when our clients complains that application is "freeze PC for a few minutes".

This problem appears only if you have significant number of items (we talking about hundreds of thousands here) stored in one collection.

To understand why this happening you need take closer look at TCollectionItem.Destroy, TCollectionItem.SetCollection and TCollection.RemoveItem functions which located at classes.pas - last one is the key to understanding this problem.

You also might want to compare your TCollection.RemoveItem version to Delphi 6 TCollection.RemoveItem code:

{ classes.pas from Delphi 6 } 
procedure TCollection.RemoveItem(Item: TCollectionItem); 
begin 
   Notify(Item, cnExtracting); 
   if Item = FItems.Last then 
      FItems.Delete(FItems.Count - 1) // that will fix original problem 
   else 
      FItems.Remove(Item); 
   Item.FCollection := nil; 
   NotifyDesigner(Self, Item, opRemove); 
   Changed; 
end;  

Now you probably will want to fix it. But seems it is not so easy because TCollection.RemoveItem is not a virtual or dynamic function.

Here is two solutions:

[1] You will need to alter classes.pas - put that TCollection.RemoveItem code from Delphi 6 into your version of classes.pas.

Copy new (fixed) classes.pas into your project directory and put it at the first position in your .dpr uses section like this:

program Project1; 

uses 
   classes in 'classes.pas' // new classes.pas with fixed TCollection.RemoveItem 
   Forms, 
   Unit1 in 'Unit1.pas' {Form1}; 

{$R *.res} 

begin 
   Application.Initialize; 
   Application.CreateForm(TForm1, Form1); 
   Application.Run; 
end.  

Now your project will be compiled with new version of TCollection.

[2] In some situations it is not so convenient or even not possible to use altered classes.pas and in this case we have other trick for you.

type 
{ TFixCollection - fix TCollection.RemoveItem issue in Delphi 3,4,5 } 
   TFixCollection = class(TCollection) 
   public 
   { Unfortunately Clear is not a virtual or dynamic procedure so we will have to reintroduce it } 
      procedure Clear; 
      destructor Destroy; override; 
   end; 

procedure TFixCollection.Clear; 
var 
   i: integer; 
   AList, OrgList: TList; 
begin 
   AList := TList.Create; 
   try 
      OrgList := TList(PDWORD(DWORD(Self) + $4 + SizeOf(TPersistent))^); 

      { Save original pointers to collection items } 
      for i := 0 to OrgList.Count-1 do 
         AList.Add(OrgList[i]); 

      OrgList.Clear; 

      { Destroy collection items } 
      for i := 0 to AList.Count-1 do 
         TCollectionItem(AList[i]).Free; 
   finally 
      AList.Free; 
   end; 
   inherited; 
end; 

destructor TFixCollection.Destroy; 
begin 
   Clear; 
   inherited; 
end; 

{ Let's try again ! } 
procedure TForm1.Button2Click(Sender: TObject); 
var 
   old: TFixCollection; 
   i: integer; 
begin 
   old := TFixCollection.Create(TCollectionItem); 
   for i := 0 to 100000 do 
      old.Add; 
   Windows.beep(900, 1000); // hi-freq beep after we done with adding empty items 
   old.Free; 
   Windows.beep(100, 1000);// low-freq beep after we done with destroying empty items 
end;  

As you can see now it works just fine.
We used one trick which gives us access to protected section of TCollection.

Both solutions was tested with Delphi versions 3,4 and 5 but the best advice would be migration to Delphi 6 or 7 of course.

For your convenience you can download Delphi 3,4,5 TCollection performance issue demo sources here CollectionPerformanceDemo.zip

Please visit our website for more articles.

Clever Components Support Team.
www.CleverComponents.com.
info@clevercomponents.com.

Server Response from: ETNASC04