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; }
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!
3 Trackbacks
[…] Object Pooling I’ve added object pooling as described here to the flamethrower and antimatter gun, both of which were spitting out a ton of new objects per […]
[…] I worked on some optimisations for the above. This included adding object pooling for every object that’s generated when an enemy is destroyed, combining multiple smaller […]
[…] in optimising performance is that all objects used in these calculations are recycled as described here, so there is no object allocation or garbage collection involved. I’ve found that recycling […]