From: skolima on June 08, 2007, 01:28:26 pm

And now, rant about ##478 (SetProcessWorkingSetSize?).

I did a little research about this solution - it seems EveMon? gains nothing here (well yes, it does decrease the number shown in 'Mem Usage' column in Process Explorer, which is BTW a bogus value), but loses performance and portability.

Mangling with GC and working set size is generally not a good idea. People behind .Net and the OS spend quite a lot of time tuning memory management so that it is as fast and as lightweight as possible. When left on "auto", the garbage collection occurs when the cpu is idle and the working set gets trimmed when memory is needed by other processes. By mangling with this, EveMon? produces more stress on the machine instead of less. And calling SetProcessWorkingSetSize?(HWND, -1, -1) is a pure loss of resources - we release all memory the .Net manager has available and force ourselves into the page file. Which means that anything EveMon? does next demands the memory back from the system allocator, probably fragmenting the working set. Not to mention that this method destroys all WeakReferences? (which EveMon? uses for caching some objects).


skolima, you bring up a number of good points. Microsoft strongly advises that you don't normally muck around with the garbage collector and simply let it do its work tuning the system. The garbage collector is very good at adapting to the memory allocation patterns of a program and optimizing to it, including getting the most value out of the L1 CPU cache. It does a far better job than 99.9% of programmers will do, only the people who work on high end games (like Doom, Quake) tend to put more effort into managing memory allocations and can outperform the GC.

That said, the GC has two big limitations:

  • It cannot tell "program intent". It does not know when a program is idle vs. when a program is temporarily paused waiting for "the next big thing" to happen
  • It cannot optimize outside of its own process. It tends to act as if this running program is the only running program on the machine.

Given this, it can produce less than optimal performance on a computer, especially when being used as "terminate and stay resident" notification programs that sit in the system tray. You can see this happening if a program allocates a lot of memory (say a complex Windows form) and then goes idle. Or say if it sits in the background and chomps on an Xml document casually. EVEMon happens to fit both these categories.

If you get into a performance optimization talk with one of the architects of the CLR, you'll hear statements like "Don't call GC.Collect(). Well, unless you've just finished a big operation (like closing a window, flipping between window contents) or unless you know something of your intent such as you've just finished a big operation and you're going idle." The subtlties of the "well, unless" get dropped in things like MSDN articles where the intent is to get that 99.9% of the programmers doing "approximately" the right thing. These details tend to get omitted from the public articles on purpose because we developers have a strong tendency to "over optimize" and to think that we're all Michael Abrash (of Id/Doom fame, one of the dudes that optimizes his memory allocations on 32 bit boundaries and uses MMX to perform memcopies because it's faster :-) ) and that we're "smarter than your average Garbage Collector".

The GC optimization to EVEMon was written that way for two reasons. First, we had some serious performance problems with EVEMon, centering mostly around XML and Windows Forms allocating a lot of memory and then going idle without releasing the memory. The GC only collects memory when the program is active (when a memory allocation is performed, actually) and when it detects memory contention (stuff that needs to be moved between generations, memory getting low) and this was ending up in a mostly idle program with a very large working set that was interfering with other applications on the machine. I run 1 GB and 2 GB machines and *I* was feeling a drop in my other app performance when EVEMon was sitting in my system tray. Second, EVEMon does occasional (this didn't used to be occasional, it used to do non-trivial processing once per second even when no-one was looking, but that's a different bug/discussion) background work while sitting in the system tray and it needs to be trimmed after this work is finished and it goes idle. The use of a dirty timer is a simple way of detecting "now I'm truly idle" in an indeterministic world.

Regarding the SetProcessWorkingSetSize? call, this is an optimization call that Windows Explorer makes on every process when a Window is minimized. I know this because I stole the code from Windows Explorer and talked about it with the dev who wrote it :-) So this is doing nothing more than what Windows normally does when it thinks your process has gone idle. I put it in the timer because while Windows knows when a Window is minimized and can optimize this automatically, it knows nothing about "I just downloaded an Xml file from a server and processed it while sitting in the system tray" and can't auto-optimize you.

Regarding #ifdef'ing out the call to memory optimization on the Mono port, this sounds like a good idea. Replacing it with a Mono-portable memory optimization call might be a better approach. If at the end of the day, EVEMon runs efficiently on Windows but causes Macintosh and Linux systems to run more slowly, I guess I'm OK with that too :-)


Thank you Nimrel for your comprehensive response. I have to admit you convinced me. I also did some tests for the WeakReferenece? business - turns out GC is smarter than i though, simple GC.Collect's are not enough to void the WeakReferences?. Which is very good news.

The only valid remark I have to the AutoShrink? it to use

if (Environment.OSVersion.Platform == PlatformID.Win32NT)