In this tutorial, we will see how you can use dotMemory to optimize your application's memory usage.
What do we mean by "optimizing memory usage"? Like any process in the operating system, Garbage Collector (GC) consumes system resources. The logic is simple: the more collections GC has to make, the larger the CPU overhead and the poorer app performance. Typically, this happens when your app allocates a large number of objects that are required for some limited period of time.
To identify and analyze such issues, you should examine the so-called memory traffic. Traffic information shows you how many objects (and memory) were allocated and released during the selected timeframe.
Let’s see how we can determine excessive allocations in your app and get rid of them using dotMemory.
Traditionally, the sample app we’ll use for this tutorial is Conway’s Game of Life. Before you begin, please download and unpack this zip package with the app.
As the app works with a large number of objects (cells), it would be interesting to look at the dynamics of how these objects are allocated and collected.
Step 1. Running the Profiler
- Open the Game of Life solution in Visual Studio.
- Run dotMemory using the menu ReSharper | Profile | Profile Startup Project Memory Profiling.
- In the opened Profiler Configuraion window, turn on Start collecting allocation data immediately. This will tell dotMemory to start collecting profiling info right after the app is launched. This is how the window should look like after you specify the options:
- Click Run to start the profiling session. This will launch our app and open the main Analysis page in dotMemory:
Switch to the dotMemory’s main window to see the timeline. The timeline shows you the memory usage of your app in real time. More specifically, it provides details on the current size of unmanaged memory*, Gen0, Gen1, Gen2 heaps and Large Object Heap. Up until Game of Life starts, memory consumption stands still:
Step 2. Getting Snapshots
After the app is launched, we can start getting memory snapshots. As we want to investigate the dynamics of how our app behaves, we need to take at least two snapshots. The timeframe between getting the snapshots will be the subject of further memory traffic analysis.
Naturally, both snapshots must be taken during that part of Game of Life's operation when the majority of allocations occur. Let’s take one snapshot at the 30th generation of the Game of Life, and the second one at the 100th generation.
- Start the game using the Start button in the app.
When the Generations counter (in the top right-hand corner of our app) reaches 30*, click the Get Snapshot button in dotMemory.
If you now look at the timeline, you’ll see how the app consumes memory in real time. When the app allocates new objects, memory consumption increases (Gen0 diagram grows). When garbage collection takes place, memory consumption decreases. As a result, the timeline follows a saw like pattern.
- When the Generations counter reaches 100, get one more snapshot, again by using the Get Snapshot button in dotMemory.
- End the profiling session by closing the Game of Life app.
The main page now contains two snapshots:
Step 3. Analyzing Memory Traffic
Now, we’ll take a look at the memory traffic in the timeframe between getting the snapshots.
- Click Add to comparison for each snapshot to add them to the comparison area. The order in which you add snapshots is not important, as dotMemory always uses the older snapshot as the basis for comparison:
- Click View memory traffic in the comparison area:
This will open the Traffic view. The view currently shows us how many objects of a certain type were created between Snapshot #1 and Snapshot #2.
Take a look at the list. 23 MB, or about 50% of the overall memory traffic, is due to the allocation of objects of the
Cellclass*. At the same time, most of these
Cells – 22.7 MB – were collected as well. That's quite strange, since cells should exist for the whole duration of any Game of Life. There is no doubt that these collections are hurting our app's performance.
Let’s check where these
Cellobjects come from.
- Click the row with the
The list at the bottom of this screen shows us the function (back trace) that created the objects. Apparently, this is the
CalculateNextGenerationmethod of the
Gridclass. Let’s find it in the code.
- Open the GameOfLife solution in Visual Studio.
- Open the Grid.cs file which contains the implementation of the
CalculateNextGeneration(int row, int column)method:
It appears that this method calculates and returns the
Cellobjects for each next generation of Game of Life. But this doesn’t explain high memory traffic. Let’s return to dotMemory and find out what function calls the
- In dotMemory, expand the
CalculateNextGenerationmethod to see the next function in the stack.
It is the
Updatemethod of the
Find this method in the code:
This finally sheds light on the causes of our high memory traffic. There is the
nextGenerationCellsarray of the
Celltype which stores cells for the next generation of Game of Life. On each generation update, cells in this array are replaced with new ones. Cells left from previous generation are no longer needed and get collected by GC after some time.
Obviously, there’s no need to fill the
nextGenerationCellsarray with new cells each time as the array exists during the entire lifetime of the app. To get rid of high memory traffic, we simply need to update the properties of existing cells with new values instead of creating new cells. Let’s do this in the code.
Actually, as our app is a learning example, it already contains the required implementation of the
CalculateNextGenerationmethod. This method updates a cell’s
Agefields sent by reference:
To fix the issue, simply uncomment the lines in
Update()that update the
nextGenerationCellsarray using this method. Finally, the
Update()method should look as follows:
Now, let's apply these changes and check how they affect the memory traffic.
- Build the app one more time. Repeat the steps described in Step 1. Running a Profiler and Step 2. Getting Snapshots to get two new snapshots.
- Open the Traffic view to see the memory traffic between the collected snapshots (as described in Sub-steps 1 and 2 in Step 3. Analyzing Memory Traffic):
GameOfLife.Cellclass is no longer on the list! This resulted in a 35% drop in the overall traffic (down to 34 MB), which is a very good optimization.