Back when I was starting my career as a developer at IBM, the department’s guru (bearded, of course) gave me some advice which has stuck in my mind: “first you make it work, then you make it fast”. I found the advice dubious at the time, but it was a commonly held belief at IBM and I eventually adopted it.
Clearly, this advice dates from a much quainter time. IBM’s metric back then was 100 lines of code per day (anything more and you clearly weren’t testing properly), and we were actually given time to make it fast after we made it work.
Today, the practice is more likely to be “first you make it work, then you move on to the next project”. Performance optimization is something that needs to be done the first (and only) time you write the code. This is, of course, easier said than done, and despite our best intentions version 1.0 of any sizable application is likely to leave room for improvement when it comes to performance.
A couple of weeks ago, a user of Version 1.0 of our .Net application called the support line to report that one of our screens took half an hour to load. I knew our performance wasn’t that bad, so I tried loading the same data on my PC and, while it took a little over 3 minutes, I noticed that memory usage shot up to over 1 gigabyte. This user had only 1 gig of RAM on her PC, so the mystery was solved. Personally, I was kind of proud that the application diligently toiled away until it was able to fit a gig of data into a 1 gig PC, but the account executive thought it might be nice if we could shrink that gig of data into something more managable. After spending a couple of weeks giving my app a liposuction, I thought I’d share some a few tips.
1. Before firing, aim. That is, before trying to solve the problem, be sure you know what the cause is. The culprit may surprise you — in fact, assuming that you’ve done a fairly good job of writing memory efficient code, the culprit will almost certainly surprise you. I found the approach described in this article to be a quick way of getting to the root of the problem, without the need to buy and learn a commercial profiling tool. In a nutshell, you can use the Windows Performance Monitor to confirm that it is .Net code that is gobbling up the memory, as opposed to native code such as a 3rd party tool or a legacy DLL. If the problem is in managed code, you can use Microsoft’s WinDbg tool, and in particular its dumpheap command, to see what types of data are using the most RAM.
In the case of my application, as shown by the Performance Monitor snapshot at the right, the native code (in blue) used memory sparingly, but managed code (in yellow) shot to the moon. The dumpheap command uncovered the following list of suspects (where the 3rd column is memory usage, in bytes):
04eaec84 429440 29201920 TV8_Demo.clsRating
05f9092c 429440 30919680 TV8_Demo.BLstBaseRating
07f8dcfc 336132 44369424 Infragistics.Win.UltraWinGrid.UltraGridCell
790fd8c4 1310318 52389724 System.String
07e1fcb4 339952 89747328 Infragistics.Win.Appearance
7912d8f8 690781 96179680 System.Object[]
093d522c 102219 136564584 TV8_Demo.clsBookingDetail
010c340c 858880 147727360 System.Nullable`1[[System.Single, mscorlib]][]
2. Everything counts in small amounts. This isn’t news to any experienced programmer, but your choice of data types matters when your code base grows to tens of thousands of variables. It is very easy to get in the habit of choosing variable data types for comfort rather than speed. If you make all your numeric variables “int” or “decimal”, and all your text variables “string”, your coding will be a lot easier and your app will be a lot fatter. It is far better to get in the habit of using byte, sbyte, float and char variables and doing the extra casting and conversion code that they require — it’s a pain, but that’s why they pay us the big bucks!
Sometimes a more imaginative solution is called for. One of my classes, clsBookingDetail (the 2nd worst offender identified by dumpheap), is used as the basis for a weekly grid and therefore contained 52 float variables. Since only a small percentage of weeks contained values in each record, I was able to save a lot of memory by replacing the 52 variables with get/set fields, where the get and set method retrieved the value from a hashtable.
Another heavily used class contained an array of float? variables. This is the “System.Nullable” object that was chewing up 147M of heap space, and my first thought was that using a nullable type must be much more expensive than the underlying value type. It was, but savings weren’t dramatic. The real problem here was the size of the arrays — they, too, were sparsely populated, and a large proportion of these arrays contained nothing but nulls. A Hashtable would have been a better choice here, as well, but I decided to do a bit of extra work and rewrite the code so that the arrays would be dynamically sized and contain only the non-null values. These changes reduced that 147M down to 19M.
3. Don’t ignore 3rd party tools when optimizing your application. Here’s a nightmare that haunts any developer: a 3rd party tool that covertly causes performance problems. It’s never fun to have to respond to user problems by pointing your finger at someone else, especially if you can prove that you’re right. If the 3rd party tool consists of managed code, then dumpheap makes this scenario much less scary. When you can tell the developer of the 3rd party tool exactly what the problem is, they are likely to have heard about it from other customers and will be happy to provide you with a solution. In my case, 2 of the 6 largest users of the heap were objects that are part of the Infragistics WinAdvantage suite: the grid’s UltraGridCell object, and the Appearance object that is used to set a cell’s display attributes. A visit to the Infragistics support forum uncovered a few suggestions for more efficiently using these objects, reducing the number of Cell objects on the heap by 1/3, and the number of Appearance objects to almost zero.
4. Sweat the small stuff. That is, be very, very careful about the size of small objects and structures that are heavily used in your application. Early in the development in our application I had created a simple structure that was used to associate variables with their respective table, record and column of the Dataset. I had used a few “int” and “enum” variables, then largely forgot about this structure since it served its purpose well. This structure was so insignificant that it didn’t come immediately to mind when I saw the results of the dumpheap command — structures and value types aren’t identified separately in the heap, but are lumped into the objects which own them,such as clsBookingDetail, BLstBaseRating and clsRating in the above list. When I finally turned my attention to this piddly little structure, replacing the “int” variables with “sbyte” and “int16″ variables, and assigning a “byte” data type to its enums, I was startled to find that this minor changes reduced the size of these 3 classes on the heap by about 25%.
There is a lot more to be said on this topic, but in the case of my application these “low hanging fruit” accounted for an improvement of about 50% in memory usage. Dumpheap and the other parts of the Windbg toolset are something that is particularly worth learning, and they deserve a much higher profile than they get in .Net development books. For a top notch tutorial on how to use Windbg to diagnose and fix application performance problems, see the .Net Debugging Tutorials in the “If broken it is, fix it you should” blog.
Hey Dan thanks for the article, its very well-written and explained.
I am an asp.net developer so i don’t have to worry so much about memory-management. Nevertheless this article is very useful and it will surely help me with my websites.
Thanks again.
you mention “A visit to the Infragistics support forum uncovered a few suggestions for more efficiently using these objects, reducing the number of Cell objects on the heap by 1/3, and the number of Appearance objects to almost zero.”
Can you point me in the right direction of what you did? especially the appearance objects, there are tons of those by default.
Yes, sorry, I should have mentioned the solution in the article.
When you apply a change to the appearance of an UltraGrid cell (or any other grid objects, such as a row or a column header), the Infragistics code automatically generates a new Appearance object. For whatever reason, that Appearance object hangs around in memory afterwards. So, code like this:
for (int i = 0; i < gridBuyLines.ActiveRow.Cells.Count; i++)
{
if (gridBuyLines.ActiveRow.Cells[i].Text == “ColourMeRed”)
gridBuyLines.ActiveRow.Cells[i].Appearance.ForeColor = Color.Red;
}
… is going to generate a bunch of Appearance objects and leave them on the heap.
It is far more memory efficient to do this instead:
Infragistics.Win.Appearance redCell = gridBuyLines.ActiveRow.Cells[0].Appearance;
redCell.ForeColor = Color.Red;
for (int i = 0; i < gridBuyLines.ActiveRow.Cells.Count; i++)
{
if (gridBuyLines.ActiveRow.Cells[i].Text == “ColourMeRed”)
gridBuyLines.ActiveRow.Cells[i].Appearance = redCell;
}
Of course, this approach is also pain to code, since you need a difference Appearance object for each combination of ForeColor, BackColor, FontData, etc. Although I hated having to do it, I ended up writing a long “CreateAppearances” method that creates all of the appearance objects that my app needs:
cellAppearance = (Infragistics.Win.Appearance)gridBuyLines.DisplayLayout.Bands[0].Columns["StrStation"].CellAppearance.Clone();
altMonthCellAppearance = (Infragistics.Win.Appearance)cellAppearance.Clone();
altMonthCellAppearance.BackColor = Color.Gainsboro;
// accounting activity:
paidCellAppearance = (Infragistics.Win.Appearance)cellAppearance.Clone();
paidCellAppearance.ForeColor = colorPaid;
paidCellAppearance.FontData.Bold = DefaultableBoolean.True;
etc, etc.
Crude, but effective.
Dan.