Optimising Memory Use In MonoGame

Don’t we all love the garbage collector in .net? It frees us from that irksome task of allocating and releasing memory ourselves and (virtually) eradicates memory leaks. Hooray for the garbage man!

Unfortunately there’s no such thing as a free lunch and all this ease of use comes with a performance penalty – and potentially quite a nasty one at that. Each time the garbage collector kicks in there’s a small CPU spike which can cause an inconsistent frame rate or the appearance of ‘juddering’ in your game.

The solution is to try and make sure as little garbage build up as possible and (if necessary) manually trigger collection at points in your game where there’s a natural pause, for example on losing a life or completing a level.

So, here’s a few tips on minimising the garbage… I welcome any feedback on any of these or any other suggestions.

1. Reuse and Recycle
Any game is bound to have a bunch of objects that are used and discarded on a regular basis. Sprites, rectangles, animations etc etc. Rather than creating and discarding these on an ‘as needed’ basis create a ‘pool’ of objects up front and retrieve and return them to the pool as required. This approach obviates the need for both memory allocation and garbage collection for the applicable objects in-game and can have a very positive impact on game performance.

Of course this approach comes with a development overhead, you are now (effectively) back to managing memory yourself and have to be very careful that you don’t attempt to re-use an object once it has been returned to the pool and re-initialized. These types of bugs can be very tricky to track down!

Below is the template I use for a simple generically-typed object pooling system consisting of an ObjectPool class and an IPoolable interface.

using System;
using System.Collections.Generic;

namespace com.bitbull.objectpool
{
	/*
	 * Any object that is to be pooled needs to implement this
	 * interface and support a parameterless constructor.
	 */
	public interface IPoolable
	{
		/*
		 Should reset all the object's properties to their default state.
		 */
		void Initialize();

		/*
		 Called when an object is returned to the pool. The implementation of 
		 this method doesn't need to do anything but it can be useful for certain
		 cleanup operations (possibly including releasing attached IPoolables).
		 */
		void Release();

		/*
		 Set by the pool and used as a 'sanity check' to check this object came from
		 the pool originally
		 */
		bool PoolIsValid
		{
			get;
			set;
		}

		/*
		 Used by the pool as a 'sanity check' to ensure objects aren't freed twice.
		 */
		bool PoolIsFree
		{
			get;
			set;
		}
	}
	/*
	  Template for a generically-typed object pool - pooled objects must
	  implement the IPoolable interface and support a parameterless constructor.

	  To create a new pool - ObjectPool pool = new ObjectPool(int initial_capacity)
	 */
	public class ObjectPool where T:IPoolable,new()
	{
		// I use a Stack data structure for storing the objects as it should be 
		// more efficient than List and we don't have to worry about indexing 
		private Stack stack;
		// The total capacity of the pool - this I only really use this for debugging
		private int capacity;

		/*
		 Creates a new object pool with the specifed initial number of objects
		 */
		public ObjectPool( int capacity )
		{
			stack=new Stack( capacity );

			for( int i=0; i<capacity; i++ )
			{
				AddNewObject();
			}
		}

		/*
		 Adds a new object to the pool - ideally this doesn't happen very often 
		 other than when the pool is constructed
		 */
		private void AddNewObject()
		{
			T obj=new T();
			obj.PoolIsValid=true;
			stack.Push( obj );
			capacity++;
		}

		/*
		 * Releases an object from the pool - note that there's no real need 
		 * to throw the first exception here as if an object is freed twice it's not
		 * really a problem, however the fact that this is happening usually indicates 
		 * an issue with one's memory management that could cause issues later so I 
		 * prefer to leave it in.
		 */
		public void Release( T obj )
		{
			if ( obj.PoolIsFree )
			{
				throw new Exception( "POOL ("+this+"): Object already released " + obj );
			}
			else if ( !obj.PoolIsValid )
			{
				throw new Exception( "POOL ("+this+") Object not valid " + obj );
			}
			obj.Release();
			obj.PoolIsFree=true;
			stack.Push( obj );
		}

		/*
		 * Retrieves an object from the pool - automatically create a new object if the pool
		 * has become depleted.
		 * 
		 * Calls Initialize() on the released object which should set all its parameters to
		 * their default values.
		 */
		public T Get()
		{
			if (stack.Count==0)
			{
				AddNewObject();
			}
			T obj=stack.Pop();
			obj.Initialize();
			obj.PoolIsFree=false;
			return obj;
		}
	}

}

2. Don’t Create Temporary Objects
Avoid the temptation to do stuff like this in your code (and I don’t mean the ridiculously long variable names)…

public void SomeMethod()
{
	FooBar some_temporary_object_needed_for_a_calculation = new FooBar();
	//
	// Do some stuff that needs a temporary FooBar object 
	//
	return;
}
Instead (providing your code is thread safe) make the FooBar object a ‘scratch’ class variable (so it persists for the life of the containing object) or even better a static variable (so that it persists for the lifespan of your app). Note that I don’t think there’s really any point in doing this with temporary structs as their creation/disposal is relatively trivial compared to that of classes.
private static FooBar foobar_scratch;

public void SomeMethod()
{
	//
	// Do some stuff with the 'scratch' FooBar object 
	//
	return;
}

3. Stack ‘Em Up
If you have objects that are not being pooled (because it’s too much effort or whatever) consider adding them to a Stack on creation and only emptying the stack at a point where there’s a natural pause in the game. Note that you have to watch your overall memory use with this approach as you have basically, albeit deliberately, created a massive memory leak! Only really suitable for small objects or those that aren’t created that regularly.

4. Avoid Using SetRenderTarget()
Even though it’s used in most examples SetRenderTarget() creates a bunch of garbage and should be avoided. Use SetRenderTargets() instead (even if you have only one). There’s more information on this (and some other cool tips) here.

5. Overload Your SpriteBatcher
When running some diagnostic tools on Jetboard Joust I realised that there was a bunch of wasted memory generated by the MonoGame SpriteBatch class. It seems that for every render an array is created to the size of the number of ‘draw’ operations to be executed. Whilst this array persists to an extent (much like the Collections classes a new array is only created if the old one doesn’t have enough capacity) when the amount of render operations can increase and vary considerably (for example with particle systems) you have, potentially, an awful lot of memory allocation and retrieval going on.

The solution I’ve tried for this (which appears to work) is simply to hammer your SpriteBatch with a load of render operations the first time around to ensure that an array is created that’s big enough to cover most scenarios.

6. Watch Those Strings
Though you wouldn’t think it, strings are a common cause of memory problems – particularly where there’s string concatenation involved (as there is in most debug calls). There appears to be an especially JNI-related nasty memory ‘leak’ (ie tons of garbage getting created) involving strings in the MonoGame Android implementation (though it’s deep in OpenTK rather than in the MonoGame code per se).

Be particularly wary of ‘debug’ type calls where your debug string may still be being created even if it’s not being output!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: