views:

812

answers:

11

I am working in Delphi 5 (with FastMM installed) on a Win32 project, and have recently been trying to drastically reduce the memory usage in this application. So far, I have cut the usage nearly in half, but noticed something when working on a separate task. When I minimized the application, the memory usage shrunk from 45 megs down to 1 meg, which I attributed to it paging out to disk. When I restored it and restarted working, the memory went up only to 15 megs. As I continued working, the memory usage slowly went up again, and a minimize and restore flushed it back down to 15 megs. So to my thinking, when my code tells the system to release the memory, it is still being held on to according to Windows, and the actual garbage collection doesn't kick in until a lot later.

Can anyone confirm/deny this sort of behavior? Is it possible to get the memory cleaned up programatically? If I keep using the program without doing this manual flush, I get an out of memory error after a while, and would like to eliminate that. Thanks.

Edit: I found an article on about.com that gives a lot of this as well as some links and data for other areas of memory management.

+1  A: 

I have read about this before but have no direct experience. Calling WINAPI SetProcessWorkingSetSize() is supposed to "fix" the problem. Again I have no direct experience with this.

fupsduck
From the description on the MSDN, it might be an interesting exercise (whether or not in futility, i don't know) to try using the get and then the set to force a refresh attempt.
Tom
I looked through my archive. I think this is where I saw the info: http://delphi.about.com/od/windowsshellapi/ss/setprocessworkingsetsize-delphi-program-memory-optimize.htm
fupsduck
That link is quite useful. I'm going to have to see if I can incorporate this without killing my app. Thanks.
Tom
This function is largely useful only in systems under physical memory pressure, and affects the relative importance of the indicated application's memory vis a vis other running applications. It's a bit like a process priority, except for memory. Like process priority when CPU is less than 100%, it doesn't (shouldn't) have a lot of effect in the absence of physical memory pressure except on the statistics in Task Manager.
Barry Kelly
A: 

In my understanding, Delphi does not support garbage collection in the sense that it automatically detects unreferenced objects and frees them. It does "garbage collection" when an object goes out of scope for certain types of data (much like a C++ smart pointer). So I do not believe that there is any kind of API in Delphi that you can call to force a garbage collection sweep.

It might be that FastMM releases unused memory when the application is minimized. I am not familiar with it, though, so I do not know if that is true. But you might investigate that. That seems more likely to me than Windows doing such a thing when an application is minimized.

Mark Wilkins
Delphi doesn't do the sort of garbage collection you're talking about, at least not for objects. Strings and some Interfaces are reference-counted and will be cleaned up when the count drops to zero, which can happen when their references go out of scope, but objects (at least ones that don't implement reference-counted interfaces) are freed by calling the Free method directly.
Mason Wheeler
The only problem with investigating FastMM is I don't know how well I can since a lot of it is in assembly, and I don't know that at all.
Tom
+2  A: 

Task Manager doesn't show what your program is actually using. It shows the total that the memory manager has allocated from Windows. When you free an object or otherwise deallocate dynamically allocated memory, it gets returned to the memory manager (FastMM) immediately. Whether or not that's passed back to Windows is another matter. The memory manager likes to keep some extra memory sitting around so it doesn't need to grab more from the OS every time you need to create a new object. (This is a good thing, and you don't want to change it.)

If your program's memory usage is continually increasing, instead of hitting a steady state at some point, you might want to look around and see if you're leaking memory somewhere. And as Mark mentioned, Delphi doesn't use automatic garbage collection. Just in case you weren't aware of this, make sure you're either freeing your objects or handing their ownership off to something that will free them when they're no longer needed.

Mason Wheeler
Yeah, I've been doing cleanup all week and making sure everything gets freed properly. One thing I discovered two days ago (I'm only in my 3rd year as a professional programmer) is that some of the pointer types (TStringList, etc) won't free objects attached to them, so I've been trying to clean everything like that. That cut my footprint size down big time, but it still grows and won't plateau. I suspect FastMM isn't giving back to Windows properly, but I can't be sure at this time. Going to watch it with a different monitor app.
Tom
Unlike many VCL classes TStringLists do have to be created and destroyed explicitly using new and delete.
fupsduck
Tom: Yeah. TStringList finally got object ownership in Delphi 2010, but it didn't have it back in D5. Fupsduck: Not quite. TStringList isn't a VCL class, it gets created with its constructor (.Create) and destroyed with the Free method. New and Delete are only for non-object pointers.
Mason Wheeler
sorry - I was thinking CBuilder which is what I use more than Delphi.
fupsduck
I think one of my bigger remaining problems is that I have a lot of forms in this application (created on demand, not at start up), and in the past, I've only been able to do a little experimenting with freeing them up when I don't need them and recreating them when I do. Unfortunately, this hasn't worked so well as the program has crashed on me, and I'll probably re-investigate this and find out why.
Tom
Actually it doesn't show total allocation from Windows. By default, it shows the working set.
Barry Kelly
+45  A: 

Task Manager doesn't show the total that the application has allocated from Windows. What it shows (by default) is the working set. The working set is a concept that's designed to try and minimize page file thrashing in memory-constrained conditions. It's basically all the pages in memory that the application touches on a regular basis, so to keep this application running with decent responsiveness, the OS will endeavour to keep the working set in physical memory.

On the theory that the user does not care much about the responsiveness of minimized applications, the OS trims their working set. This means that, under physical memory pressure, pages of virtual memory owned by that process are more likely to be paged out to disk (to the page file) to make room.

Most modern systems don't have paging issues for most applications for most of the time. A severely page-thrashing machine can be almost indistinguishable from a crashed machine, with many seconds or even minutes elapsing before applications respond to user input.

So the behaviour that you are seeing is Windows trimming the working set on minimization, and then increasing it back up over time as the application, restored, touches more and more pages. It's nothing like garbage collection.

If you're interested in memory usage by an application under Windows, there is no single most important number, but rather a range of relevant numbers:

  • Virtual size - this is the total amount of address space reserved by the application. Address space (i.e. what pointers point to) may be unreserved, reserved, or committed. Unreserved memory may be allocated in the future, either by a memory manager, or by loading DLLs (the DLLs have to go somewhere in memory), etc.

  • Private working set - this is the pages that are private to this application (i.e. are not shared across multiple running applications, such that a change to one is seen by all), and are part of the working set (i.e. are touched frequently by the app).

  • Shareable working set - this is the pages in the working set that are shareable, but may or may not actually be shared. For example, DLLs or packages (BPLs) may be loaded into the application's memory space. The code for these DLLs could potentially be shared across multiple processes, but if the DLL is loaded only once into a single application, then it is not actually shared. If the DLL is highly specific to this application, it is functionally equivalent to private working set.

  • Shared working set - this is the pages from the working set that are actually shared. One could image attributing the "cost" of these pages for any one application as the amount shared divided by the number of applications sharing the page.

  • Private bytes - this is the pages from the virtual address space which are committed by this application, and that aren't shared (or shareable) between applications. Pretty much every memory allocation by an application's memory manager ends up in this pool. Only pages that get used with some frequency need become part of the working set, so this number is usually larger than the private working set. A steadily increasing private bytes count indicates either a memory leak or a long-running algorithm with large space requirements.

These numbers don't represent disjoint sets. They are different ways of summarizing the states of different kinds of pages. For example, working set = private working set + shareable working set.

Which one of these numbers is most important depends on what you are constrained by. If you were trying to do I/O using memory mapped files, the virtual size will limit how much memory you can devote to the mapping. If you are in a physical-memory constrained environment, you want to minimize the working set. If you have many different instances of your application running simultaneously, you want to minimize private bytes and maximize shared bytes. If you are producing a bunch of different DLLs and BPLs, you want to be sure that they are actually shared, by making sure their load addresses don't cause them to clash and prevent sharing.

About SetProcessWorkingSetSize:

Windows usually handles the working set automatically, depending on memory pressure. The working set does not determine whether or not you're going to hit an out of memory (OOM) error. The working set used to make decisions about paging, i.e. what to keep in memory and what to leave on disk (in the case of DLLs) or page out to disk (other committed memory). It won't have any effect unless there is more virtual memory allocated than physical memory in the system.

As to its effects: if the lower bound is set high, it means the process will be hostile to other applications, and try to hog memory, in situations of physical memory pressure. This is one of the reasons why it requires a security right, PROCESS_SET_QUOTA.

If the upper bound is set low, it means that Windows won't try hard to keep pages in physical memory for this application, and that Windows may page most of it out to disk when physical memory pressure gets high.

In most situations, you don't want to change the working set details. Usually it's best to let the OS handle it. It won't prevent OOM situations. Those are usually caused by address space exhaustion, because the memory manager couldn't commit any more memory; or in systems with insufficient page file space to back committed virtual memory, when space in the page file runs out.

Barry Kelly
Barry, this is some great information for helping me understand this. Thanks. One follow up question related to the original: is it possible for me to tell Windows to trim the working set on demand? From fupsduck's link attached to his answer, it looks like it, but is that what the command is doing?
Tom
Barry, you've got a real knack for explaining complex topics. You ought to post this on your blog, so we'll have a place to refer people to in the future when they go asking about process memory. Heaven knows this sort of question comes up enough!
Mason Wheeler
I'll agree with Mason on this :)
Tom
@Mason - funny you mention this, because as soon as I read it, I posted it on my blog...giving full credit to Barry, of course.
Mick
+3  A: 

Let's get this straight: FastMM4 does not leak memory, your code might.

To know for sure, execute this instruction somewhere in your application (where FastMM4 is in the uses clause and $define ManualLeakReportingControl is set, in FastMM4Options.inc for instance):

ReportMemoryLeaksOnShutdown := True;

FastMM4 will then report at the end if you forgot to free some memory.

If you wish to know a bit more, you can watch this video from CodeRage 2: Fighting Memory Leaks for Dummies

François
I definitely won't dispute you. I'm coder number 4 on this batch of code, and still trying to figure out why certain things were done the way they were. Lots of room for memory issues in this code for sure. I've used the reporting capability of FastMM, as well as Pascal Analyzer and QATime to investigate this code as much as possible, so I know I've cleaned out a lot of issues.
Tom
Now, if you are sure FastMM4 does not report any memory leak, if you happen to run on **Vista, Windows2008 or Windows 7, there are memory issues in the OS** with Critical Sections keeping in cache a debug structure and ADO leaking memory each time you set the ConnectionString.
François
FastMM is reporting memory leaks, but they are ones I can't do anything about right now, and they are minor enough that I'm not worried about them for the time being. I've already gotten the memory footprint from 75 megs down to 40, and 20k is nothing at this point.
Tom
+3  A: 

After learning from Barry Kelly's excellent answer, try analysing your process using VMMap from Sysinternals, which can be found here. This analyses the memory usage of a single process in more detail even than Process Explorer: "VMMap is the ideal tool for developers wanting to understand and optimize their application's memory resource usage." It has a useful helpfile, too.

frogb
+1 for mentioning VMMap
Jeroen Pluimers
+7  A: 

This is what we use in DSiWin32:

procedure DSiTrimWorkingSet;
var
  hProcess: THandle;
begin
  hProcess := OpenProcess(PROCESS_SET_QUOTA, false, GetCurrentProcessId);
  try
    SetProcessWorkingSetSize(hProcess, $FFFFFFFF, $FFFFFFFF);
  finally CloseHandle(hProcess); end;
end; { DSiTrimWorkingSet }
gabr
I'll try incorporating this in. Thanks for the code.
Tom
Ok, this is doing great for trimming the memory back down. Now to figure out how to get it to work with the trickier areas of my code.
Tom
Minor update: Have had this working in the code for better than a week and we're now running an average of 5 megs ram and 40 megs virtual memory down from 75 and 75 just at startup.
Tom
A: 

procedure DSiTrimWorkingSet;

Amazing! Thank-you so much for solving this problem. Even though I was releaseing and freeing modal forms, the memory would continually increase. This procedure if implemented properly resets the memory and keeps things in check.

Just a question on this: Can the memory parameters be modified or is it necessary? Does this procedure pick an optimum memory to use?

I bow to your skill and wisdom.

Alan

Alan Richardson
From the documentation I've seen on the SetProcessWorkingSetSize function, you don't want to modify the parameters from above as they don't actually change anything, they just clear out the memory buffers and resets the memory usage to what your program actually needs.
Tom
+1  A: 

SetProcessWorkingSetSize(hProcess, $FFFFFFFF, $FFFFFFFF);

Something you should know about the implementation of this.

It only works when accessing data on a local drive or a UNC path. If using a mapped drive, the memory will appear to drop but refresh to the same size and more when a form or database refresh is called.

This drove us nuts on a system where the clients were logging onto the terminal server but accessing the data thru a mapped drive. The problem also was on workstaions using a mapped drive on the server.

Our system uses a starting program which confirms the path of the data, writes it in an ini file which is read by the actual program. It was a simple matter of changing the path to a UNC path eliminating the mapped drive and thus the problem.

Hope this finding reduces stress for you all.

Alan Richardson
That's definitely something I hadn't thought about. Just did a little bit of testing and it appears that the virtual memory is indeed not freeing up, but I am seeing the "regular" memory usage stay down nice and low. I will keep an eye on this though. Thanks.
Tom
A: 

I recently had a very similar problem with my program. See my question: Why does my Delphi program’s memory continue to grow?

Despite my being convinced it was something else, it turned out to be major memory leaks caused by just a few mistakes in my code to free the memory.

Before you do anything else, be absolutely certain you are releasing all your memory properly.

lkessler
A: 

You may find this and that article useful.

Alexander