Optimizing Your Delphi Program's Memory Usage

When writing long-running applications - the kind of programs that will spend most of the day minimized to the taskbar or system tray, it can become important not to let the program 'run away' with memory usage.

Learn how to clean up the memory used by your Delphi program using the SetProcessWorkingSetSize Windows API function.

01
of 06

What Does Windows Think About Your Program's Memory Usage?

windows taskbar manager

Take a look at the screenshot of the Windows Task Manager...

The two rightmost columns indicate CPU (time) usage and memory usage. If a process impacts on either of these severely, your system will slow down.

The kind of thing that frequently impacts on CPU usage is a program that is looping (ask any programmer that has forgotten to put a "read next" statement in a file processing loop). Those sorts of problems are usually quite easily corrected.

Memory usage, on the other hand, is not always apparent and needs to be managed more than corrected. Assume for example that a capture type program is running.

This program is used right throughout the day, possibly for telephonic capture at a help desk, or for some other reason. It just doesn’t make sense to shut it down every twenty minutes and then start it up again. It’ll be used throughout the day, although at infrequent intervals.

If that program relies on some heavy internal processing or has lots of artwork on its forms, sooner or later its memory usage is going to grow, leaving less memory for other more frequent processes, pushing up the paging activity, and ultimately slowing down the computer.

02
of 06

When to Create Forms in Your Delphi Applications

Delphi program DPR file listing auto-create forms

Let's say that you are going to design a program with the main form and two additional (modal) forms. Typically, depending on your Delphi version, Delphi is going to insert the forms into the project unit (DPR file) and will include a line to create all forms at application startup (Application.CreateForm(...)

The lines included in the project unit are by Delphi design and are great for people that are not familiar with Delphi or are just starting to use it. It's convenient and helpful. It also means that ALL the forms are going to be created when the program starts up and NOT when they are needed.

Depending on what your project is about and the functionality you have implemented a form can use a lot of memory, so forms (or in general: objects) should only be created when needed and destroyed (freed) as soon as they are no longer necessary.

If "MainForm" is the main form of the application it needs to be the only form created at startup in the above example.

Both, "DialogForm" and "OccasionalForm" need to be removed from the list of "Auto-create forms" and moved to the "Available forms" list.

03
of 06

Trimming Allocated Memory: Not as Dummy as Windows Does It

Portrait, girl lighted with colorful code
Stanislaw Pytel / Getty Images

Please note that the strategy outlined here is based on the assumption that the program in question is a real-time “capture” type program. It can, however, be easily adapted for batch type processes.

Windows and Memory Allocation

Windows has a rather inefficient way of allocating memory to its processes. It allocates memory in significantly large blocks.

Delphi has tried to minimize this and has its own memory management architecture which uses much smaller blocks but this is virtually useless in the Windows environment because the memory allocation ultimately rests with the operating system.

Once Windows has allocated a block of memory to a process, and that process frees up 99.9% of the memory, Windows will still perceive the whole block to be in use, even if only one byte of the block is actually being used. The good news is that Windows does provide a mechanism to clean up this problem. The shell provides us with an API called SetProcessWorkingSetSize. Here's the signature:

 SetProcessWorkingSetSize(
hProcess: HANDLE;
MinimumWorkingSetSize: DWORD;
MaximumWorkingSetSize: DWORD) ;

04
of 06

The All Mighty SetProcessWorkingSetSize API Function

Cropped Hands Of Businesswoman Using Laptop At Table In Office
Sirijit Jongcharoenkulchai / EyeEm / Getty Images

By definition, the SetProcessWorkingSetSize function sets the minimum and maximum working set sizes for the specified process.

This API is intended to allow a low level setting of the minimum and maximum memory boundaries for the process’s memory usage space. It does, however, have a little quirk built into it which is most fortunate.

If both the minimum and the maximum values are set to $FFFFFFFF then the API will temporarily trim the set size to 0, swapping it out of memory, and immediately as it bounces back into RAM, it will have the bare minimum amount of memory allocated to it (this all happens within a couple of nanoseconds, so to the user it should be imperceptible).

A call to this API will only be made at given intervals – not continuously, so there should be no impact at all on performance.

We need to watch out for a couple of things:

  1. The handle referred to here is the process handle NOT the main forms handle (so we can’t simply use “Handle” or “Self.Handle”).
  2. We cannot call this API indiscriminately, we need to try and call it when the program is deemed to be idle. The reason for this is that we don’t want trim memory away at the exact time that some processing (a button click, a keypress, a control show, etc.) is about to happen or is happening. If that is allowed to happen, we run a serious risk of incurring access violations.
05
of 06

Trimming Memory Usage on Force

Reflection of male hacker coding working hackathon at laptop
Hero Images / Getty Images

The SetProcessWorkingSetSize API function is intended to allow low-level setting of the minimum and maximum memory boundaries for the process’s memory usage space.

Here's a sample Delphi function that wraps the call to SetProcessWorkingSetSize:

 procedure TrimAppMemorySize;
var
  MainHandle : THandle;
begin
  try
    MainHandle := OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessID) ;
    SetProcessWorkingSetSize(MainHandle, $FFFFFFFF, $FFFFFFFF) ;
    CloseHandle(MainHandle) ;
  except
  end;
  Application.ProcessMessages;
end;

Great! Now we have the mechanism to trim the memory usage. The only other obstacle is to decide WHEN to call it.

06
of 06

TApplicationEvents OnMessage + a Timer := TrimAppMemorySize NOW

Businessman using computer in office
Morsa Images / Getty Images

In this code we have it laid down like this:

Create a global variable to hold the last recorded tick count IN THE MAIN FORM. At any time that there is any keyboard or mouse activity record the tick count.

Now, periodically check the last tick count against “Now” and if the difference between the two is greater than the period deemed to be a safe idle period, trim the memory.

 var
  LastTick: DWORD;

Drop an ApplicationEvents component on the main form. In its OnMessage event handler enter the following code:

 procedure TMainForm.ApplicationEvents1Message(var Msg: tagMSG; var Handled: Boolean) ;
begin
  case Msg.message of
    WM_RBUTTONDOWN,
    WM_RBUTTONDBLCLK,
    WM_LBUTTONDOWN,
    WM_LBUTTONDBLCLK,
    WM_KEYDOWN:
      LastTick := GetTickCount;
  end;
end;

Now decide after what period of time you will deem the program to be idle. We decided on two minutes in my case, but you can choose any period you want depending on the circumstances.

Drop a timer on the main form. Set its interval to 30000 (30 seconds) and in its “OnTimer” event put the following one-line instruction:

 procedure TMainForm.Timer1Timer(Sender: TObject) ;
begin
  if (((GetTickCount - LastTick) / 1000) > 120) or (Self.WindowState = wsMinimized) then TrimAppMemorySize;
end;

Adaptation For Long Processes Or Batch Programs

To adapt this method for long processing times or batch processes is quite simple. Normally you’ll have a good idea where a lengthy process will start (eg beginning of a loop reading through millions of database records) and where it will end (end of database read loop).

Simply disable your timer at the start of the process, and enable it again at the end of the process.