When working with XNA on the XBOX 360, managing your Garbage generation and collections is extremely important for performance. I’ve already blogged on some code-level optimisations which help with reducing garbage generated, but I’m going to show how to identify and fix using the fantastically free CLR Profiler.
The way I see it, there are 4 steps.
1. Verify 2. Profile 3. Locate 4. Fix
1. Verify
First off, you need to verify that you are generating garbage each frame. You can do this easily by displaying the value of the GC’s total memory using something like SpriteBatch.Draw(). To get the total memory allocated, use:
GC.GetTotalMemory(false) / 1024
This will return the number of Kilobytes of allocated memory. So if you have 11 megabytes allocated it will return a value of 11264.
Now when you run your XNA app, this number will increase (either fast or slow, or not at all) and then jump back down a couple of megabytes (if on windows). When it does that jump, a Garbage Collection just occurred. If GC memory is increasing very slowly (4-10 bytes/second) then that’s a fairly good place to be. Ideally you don’t want that number to change after your level has started. In some cases, especially on new, un-optimised projects, the number may tick over so fast you can’t even read it.
2. Profile
Now, go and get CLR Profiler, free, from Microsoft and extract it somewhere locally. I usually run the 32-bit version of CLR Profiler even though I’m running on 64-bit windows. I figure the XNA process is 32-bit, so it makes sense. I’ve never had any issues in the past, but your mileage may vary. If you have problems, try running the 64-bit version or as administrator if you’re using Vista or Windows 7.

Once you start it, select your XNA application executable and it will launch your app. Depending on how fast your allocations are occurring you may only need to play for a couple of seconds or a couple of minutes. Generally I like to profile around 3-4 collections before exiting.
When you exit, CLR Profiler will analyse the data collected from your app and then display a summary window.

From there, the first thing I look at is the Time Line. This will show me my allocations over time and GC’s will appear as spikes made up of the types that were allocated.

You can safely ignore the initial section of the graph, as allocations are expected when you load resources (textures, models, allocate initial strings etc) and GC’s during this period are not really an issue. You will need to identify the point in time of the section you want to profile, typically this will be the section that you added the GC.GetTotalMemory() to show up. In the above graph, it starts at approximately 13 seconds into the run.
You can use the Radio buttons at the top of the window to get more detail. Usually 20 or even 10 on both groups is a decent detail level. Use the scrollbars to slide right and down a bit to view one of the spikes at the top of the graph. In the below example, because I had so many collections the spikes were very narrow at horizontal setting 20, so I went down to 5 to make them wider and see more detail.

Now select and drag a section in which you can clearly see a 'step' or 'jump' up in the spike. This ‘step’ up represents a new object allocated. In the above picture I’ve selected a section between 15.183 seconds and 15.225 seconds, represented by the vertical bars.
On the right hand side, you will see a list of types and allocated sizes. When you select a small section of the graph (the 'step up' bit) it will only show allocations that occur within that selection. So in the above graph we can clearly see that over 42 milliseconds 94208 bytes of System.Single[] was allocated. That’s 2.1MB of garbage per SECOND, which is why there are so many collections occurring in my sample.
So, lets find out where it’s being allocated, and fix it.
3. Locate
Right click on the highest allocated type line on the right, and choose “Show who allocated”. In the above picture, it’s the System.Single[] line. You should see something similar to:

Scroll all the way to the right and you’ll see your hotly allocated type:

Start to follow the chain to the immediate left and you’ll see the method hierarchy that ends up allocating that type. In the case above, it’s OrientedBoundingBox.Intersects. Armed with the method, and the type (System.Single[] aka float[]) we can take a quick look at the method and..
public bool Intersects(OrientedBoundingBox oobb1, Matrix oobb1Matrix, Oriented…
{
seperating = Vector3.Zero;
worldAMin = Vector3.Transform(oobb1.Min, oobb1Matrix);
worldAMax = Vector3.Transform(oobb1.Max, oobb1Matrix);
worldBMin = Vector3.Transform(oobb2.Min, oobb2Matrix);
worldBMax = Vector3.Transform(oobb2.Max, oobb2Matrix);
centerA = (worldAMax + worldAMin) * 0.5f;
centerB = (worldBMax + worldBMin) * 0.5f;
halfExtentA = (worldAMax - worldAMin) * 0.5f; //centerA - oobb1.Min
halfExtentB = (worldBMax - worldBMin) * 0.5f;
float[] overlappingExtents = new float[15];
Vector3 centerAtoB = centerB - centerA;
It’s quickly evident that declaring a new array of 15 floats each time this method is called is the culprit, especially since this method is called potentially thousands of times per frame depending on how many active objects there are.
4. Fix
In this case it’s quite easily fixed, simply by moving the overlappingExtents declaration from within the method body to outside (so it’s a private field on the OrientedBoundingBox class) means it’s only ever allocated once when the class is created.
We can tell from logic further down in the method that all 15 values are replaced before the values are used, so we don’t have any issue with data hanging around between calls to Intersects. Running another profiling session after moving that one line of code results in a much happier graph:

This scenario was one that I identified and fixed during a long GC optimisation session which resulted in the current version of my game only allocating around 3-5 bytes per second which made my Xbox very happy. There were a lot of results from that big GC optimisation session, one of which was my previously blogged ResourcePool class.
However, there are more involved scenarios relating to string usage especially that need more creative approaches to fixing, but hopefully this has explained enough to help you find where your collections are occurring and some steps to resolve them.
58c380b8-9020-4956-9e22-ff8562d80002|4|5.0
Tags:
XNA,
Optimisation,
Garbage Collection
Categories:
General |
XNA