Using Object Pooling to Reduce Garbage Collection
Unity’s garbage collector is old and slow. As garbage from your scripts piles up, the garbage collector will eventually run. When it does, it runs all at once instead of spreading the work out over multiple frames. This means you get a big spike on one frame causing your game to momentarily freeze. One of the best ways to get around this is to use an “object pool” to reduce the number of objects eligible for garbage collection. After all, if there’s no garbage then there’s nothing to collect! For more on this strategy as well as a class you can use to implement it, read on!
The general idea of the “object pooling” strategy is to prevent garbage from ever being created so the garbage collector has nothing to collect and therefore no need to run. Normally, when you’ve released all your references to a class instance then it will become garbage. When enough garbage piles up, the garbage collector will run and usually cause a hitch in your game’s frame rate.
To accomplish this, we use an “object pool” for two purposes. First, when an object is no longer needed the object pool can keep holding a reference to it so that it won’t be considered garbage. Second, the object pool allows for the references it holds to be reused to prevent the need to allocate more memory. Together, this means that overall memory usage stays low while no garbage is ever created.
To implement an object pool, all we really need is a list of objects. A primitive pool would therefore look something like this:
// Make an empty pool var pool = new Stack<Person>(); // Make a Person directly var person = new Person("Fred", "Flintstone"); person.SayHello(); // Done with the Person, put it in the object pool pool.Push(person); // Need a new Person, grab one from the pool person = pool.Pop(); // Use the Person from the pool person.First = "Barney"; person.Last = "Rubble"; person.SayHello();
This approach has several problems. First, the pool isn’t exactly safe or easy to use. If we try to Pop
a person off to use and the Stack
is empty, we’ll get an exception. If we put in code to check that the pool isn’t empty, we’ve cluttered up our code.
Let’s address these issues by making the pool into a class:
public class ObjectPool { private Stack<Person> objects; public ObjectPool() { // Initially nothing in the pool objects = new Stack<Person>(); } public Person Get() { // If empty, make a new Person // Otherwise, get one from the pool return objects.Count == 0 ? new Person() : objects.Pop(); } public void Release(Person person) { // Add to the pool objects.Push(person); } } // Make an empty pool var pool = new ObjectPool(); // Get from the pool (creates a new Person) var person = pool.Get(); person.First = "Fred"; person.Last = "Flintstone"; person.SayHello(); // Done, release back into the pool pool.Release(person); // Get from the pool (reuses the last Person) person = pool.Get(); person.First = "Barney"; person.Last = "Rubble"; person.SayHello();
This is a lot better, but still has issues. The ObjectPool
can only hold Person
objects. We can fix that using generics like so:
public class ObjectPool<TObject> where TObject : class, new() { private Stack<TObject> objects; public ObjectPool() { // Initially nothing in the pool objects = new Stack<TObject>(); } public TObject Get() { // If empty, make a new object // Otherwise, get one from the pool return objects.Count == 0 ? new TObject() : objects.Pop(); } public void Release(TObject person) { // Add to the pool objects.Push(person); } } // Make an empty pool var pool = new ObjectPool<Person>(); // ... usage is the same
This is way better, but still not quite where it needs to be. When we switched from directly using the new
operator to using ObjectPool.Get
, we lost our ability to pass instantiation parameters to the Person
class. So let’s add that ability to ObjectPool
by allowing for an initialization structure to be passed in to Get
. We don’t need to worry about the structure itself being garbage since struct
is allocated on the stack and automatically released like other local variables without going through the garbage collector. Here’s how ObjectPool
looks now:
public interface IPoolableObject<TInitArgs> where TInitArgs : struct { void Init(TInitArgs initArgs); } public class ObjectPool<TObject, TInitArgs> where TObject : class, IPoolableObject<TInitArgs>, new() where TInitArgs : struct { private Stack<TObject> objects; public ObjectPool() { // Initially nothing in the pool objects = new Stack<TObject>(); } public TObject Get(TInitArgs initArgs) { // If empty, make a new object // Otherwise, get one from the pool var obj = objects.Count == 0 ? new TObject() : objects.Pop(); obj.Init(initArgs); return obj; } public void Release(TObject person) { // Add to the pool objects.Push(person); } } public struct PersonInitArgs { public string First { get; private set; } public string Last { get; private set; } public PersonInitArgs(string first, string last) { First = first; Last = last; } } public class Person : IPoolableObject<PersonInitArgs> { private string first; private string last; public void Init(PersonInitArgs initArgs) { first = initArgs.First; last = initArgs.Last; } public void SayHello() { Debug.Log("Hello, my name is " + first + " " + last); } } // Make an empty pool var pool = new ObjectPool<Person, PersonInitArgs>(); // Get from the pool (creates a new Person) var person = pool.Get(new PersonInitArgs("Fred", "Flintstone")); person.SayHello(); // Done, release back into the pool pool.Release(person); // Get from the pool (reuses the last Person) person = pool.Get(new PersonInitArgs("Barney", "Rubble")); person.SayHello();
This step made ObjectPool
a lot safer to use by ensuring that it’s always passed the parameters it needs when being initialized. It’s much harder to accidentally forget to set one of its fields—First
, Last
—when getting one from the pool.
Fianlly, let’s allow for pooled objects to clean themselves up. Since we’re reusing instances, we should make sure to return to a default state when released by the user. To do this, we simply add a Release
function to the IPoolableObject
interface yielding the final version of the object pool code. You’re free to use it in your projects according to the MIT license.
//////////////////////////////////////////////////// // Object pooling system by Jackson Dunstan // Article: http://JacksonDunstan.com/articles/3245 // License: MIT //////////////////////////////////////////////////// using System.Collections.Generic; /// <summary> /// An object that can be put in a <see cref="ObjectPool{TObject,TInitArgs}"/> /// </summary> public interface IPoolableObject<TInitArgs> where TInitArgs : struct { /// <summary> /// Initialize the object /// </summary> /// <param name="initArgs">Arguments to initialize with</param> void Init(TInitArgs initArgs); /// <summary> /// Release the object /// </summary> void Release(); } /// <summary> /// A pool of objects to make reusing them easier /// </summary> public class ObjectPool<TObject, TInitArgs> where TObject : class, IPoolableObject<TInitArgs>, new() where TInitArgs : struct { /// <summary> /// Unused objects eligible for reuse /// </summary> private Stack<TObject> unused; /// <summary> /// Make the pool with no objects /// </summary> public ObjectPool() { unused = new Stack<TObject>(); } /// <summary> /// Get an object. If there are unused objects from <see cref="Release"/>, one of those will be /// reused. Otherwise, a new one will be instantiated. The object's /// <see cref="IPoolableObject.Init"/> will be called in either case. /// </summary> /// <param name="initArgs">Init arguments.</param> public TObject Get(TInitArgs initArgs) { var obj = unused.Count == 0 ? new TObject() : unused.Pop(); obj.Init(initArgs); return obj; } /// <summary> /// Release the object so it can be reused later by calling <see cref="Get"/>. A reference is /// kept to prevent the garbage collector from collecting it. Its /// <see cref="IPoolableObject.Release"/> is also called. /// </summary> /// <param name="obj">The object to release</param> public void Release(TObject obj) { obj.Release(); unused.Push(obj); } }
And here’s how you use it:
using UnityEngine; public struct PersonInitArgs { public string First { get; private set; } public string Last { get; private set; } public int Age { get; private set; } public PersonInitArgs(string first, string last, int age) { First = first; Last = last; Age = age; } } public class Person : IPoolableObject<PersonInitArgs> { private string first; private string last; private int age; public void Init(PersonInitArgs initArgs) { first = initArgs.First; last = initArgs.Last; age = initArgs.Age; } public void Release() { first = null; last = null; age = 0; } public void SayHello() { Debug.Log("Hello, my name is " + first + " " + last + " and I'm " + age + " years old"); } } public class TestScript : MonoBehaviour { void Awake() { // Make the pool var personPool = new ObjectPool<Person, PersonInitArgs>(); // Get a person from the pool (allocates) var actor = personPool.Get(new PersonInitArgs("William", "Shatner", 84)); actor.SayHello(); // Release a person back into the pool personPool.Release(actor); // You shouldn't use the person anymore, but this tests that it was cleared by its Release() actor.SayHello(); // Get a person from the pool (reuses) actor = personPool.Get(new PersonInitArgs("Chris", "Pine", 35)); actor.SayHello(); // Get another person from the pool (allocates) actor = personPool.Get(new PersonInitArgs("Zachary", "Quinto", 38)); actor.SayHello(); } }
There are, of course, some downsides to this approach. First and most obviously there is additional complexity over and above simply using the new
operator and letting the garbage collector kick in. Second, this approach still has some code “safety” issues. As noted in the example, you can still use objects you’ve sent to Release
. That could cause exceptions to be thrown or unexpected behavior to occur. There’s also the need to provide a default constructor. Since you won’t have the init arguments at this point, you’ll have to create an object that isn’t really usable. It’s definitely a violation is the “resource acquisition is initialization” idiom and yet-another price to pay with this approach.
Overall, if you’re willing to live with these downsides then an object pool can be a very effective way to stop the garbage collector from causing framerate hiccups in your game. Please feel free to take the above code, enhance it, modify it to your tastes, and use it in your game. Please share what you’ve done with it in the comments if you do!
Do you use an object pool? How does yours compare to mine? If you don’t, how do you handle garbage collection in your games? Share your thoughts in the comments!
#1 by henke37 on October 19th, 2015 ·
Someone once said that the difference between a cache and a memory leak is a functioning eviction policy. The same can be said about object pools. Remember to do release excessive capacity.
On the topic of garbage collectors, it is useful to know the criteria for running them.
#2 by jackson on October 19th, 2015 ·
The two extra functions I had in mind to enhance this object pool class were related to just this. First, a
Clear
function to clear out the unused objects. Second, a maximum size of the unused object pool. That should put a cap on the memory growth, a warning when there are two many, and a mechanism to easily release all the references when you’re ready for a GC run.Do you happen to know the criteria for when Unity’s GC runs?
#3 by Vishwas on October 19th, 2015 ·
Hi,
Thanks for the article. :)
I am not a C# programmer. But I think event-listeners or similar is used in every programming language. And they too, add up to the garbage. In fact they are one of the major concerns in Actionscript.
In the current example, I don’t see event-listeners being pooled. So, would it be advisable to re-use them too, by object-pooling strategy ?
#4 by jackson on October 19th, 2015 ·
You’re welcome. :)
In an upcoming article I’ll be discussing the kinds of normal operations that cause garbage to be created and not created. Since you mention it, I’ll definitely cover events. Stay tuned!
#5 by Tronster on October 19th, 2015 ·
Great coverage of object pooling.
The explicit Init() function in a language that supports CTORs always felt clunky, but since C# doesn’t have in place construction (like C++) it will have to do. Perhaps there is a way to fake this in C# but from a quick Google, it doesn’t look like anyone has found a “clean” technique (e.g., not having to emit IL ops codes).
#6 by jackson on October 19th, 2015 ·
I too don’t like the
Init
function. As mentioned in the article, I don’t like the forced default constructor either. I just don’t know a way around this in “clean” C#. Let me know if you come up with something!#7 by Benjamin Guihaire on October 19th, 2015 ·
This is great, I would add some debug functionalities to the pool, such as a
– max number in the pool (and if the number is reached in the stack, do not push back to the pool or throw an exception)
– some debug code to catch if a “release” is done more than once.
– in the functions in the Person class, maybe add some Assert to verify that the instance currently used has not been released.
#8 by jackson on October 19th, 2015 ·
These are all great suggestions!
I mentioned the first one—a maximum number of objects—in a comment above (after you wrote your comment) as being one of the first additions I wanted to make.
The second one—checking for double-releases—could be done in
ObjectPool
by keeping track of all objects that have been released and consulting it before releasing. There’s extra CPU cost to pay, but it’s useful functionality. Perhaps some#if
preprocessor code could be used to only run it in debug modes.The third suggestion—verifying that the object isn’t released—reminds me a lot of using
IDisposable
objects. It’s common to keep abool isDisposed
. TheDispose
function sets it to true and every function begins by checking it and throwing anObjectDisposedException
if it’s set. Something similar could definitely be applied in classes implementingIPoolableObject
. Unfortunately, I don’t see a way to do it automatically in the object pool “system”, so it’s likely to be a manual process for each pooled object type.Thanks again for the suggestions!
#9 by Andrey on October 23rd, 2015 ·
Hi Jackson,
Great article! I have a few questions..
First, as far as I know, stack is index based, and therefore adding/removing elements is not that efficient, right?
And second – isn’t it a good practice to use the “who created it – he’ll dispose it” principle? As letting objects self-release sounds like a nightmare to me :)
#10 by jackson on October 23rd, 2015 ·
Stack
uses an array internally, just likeList
. CallingPush
just assigns to an index of that array, which is really fast. If the array is full, a new and longer one is allocated and the existing one is copied to that first. ThePop
function just sets an index of the array tonull
. In short,Stack
is nearly as fast as you can get and has a nice, clean interface that matches well with the needs ofObjectPool
.As for who should dispose of instances, that’s a really tricky problem. Often it’s impractical for the creator to be the disposer. In those cases you need to hand off the responsibility to dispose the instance to someone else. Making that a clearly-understood transition is the really tricky part. You don’t want both parties to assume that the other is responsible for disposing. If you do, no one will dispose it! You also don’t want them both to dispose it, as that’s often an error.
In the case of the
ObjectPool
, the creator is the pool and the disposer is whoever doesn’t callRelease
on the pool but lets the object get garbage collected. Or if you think ofRelease
as disposing, then it’s no different from any other situation involving anIDisposable
likeFileStream
. You need to come up with some way to clearly indicate who is responsible for callingDispose
orRelease
on the instance you want to get rid of.Self-release could be warranted in some situations, but usually you don’t want the objects to know about the pool they should
Release
themselves back into. Self-cleanup, on the other hand, is explicitly supported viaIPoolableObject.Release
because there’s nobody better to cleanup the internals of the instance than the class itself. Perhaps the function should be calledCleanup
or something, but that’s just a matter of naming.Hope that helps clear things up. Let me know if you’ve got any more questions and thanks for commenting! :)