Unity Pools in 2021.1 and newer

Unity Pools in 2021.1 and newer

Pools?

Pooling GameObjects has always been a thing you do in Unity. Perhaps you rolled your own pooler or obtained one from the AssetStore or Github.

But if not:

  • Pools use some extra memory to hold references to reusable objects that might ordinarily become garbage collected.

  • Constant re-constructing new versions of an object can end up fragmenting memory and causing more frequent garbage collection.

  • Performance can be improved.

Well, Unity has added object pooling as of 2021.1. How do you use it?

First of all, this isn't an easy way to get GameObject pooling for free. These pools seem to be intended as a way to pool common objects that are created over and over and would otherwise have to be GC'd. For example, you might be using a List<int> in a Monobehaviour Update method. Pooling that would be neat, eh? It's easy-peasy now.

And actually, with a little work, it's not too hard to create a prefab pooler too.

All Sorta Pools

What gives with all the different versions? There's ListPool, DictionaryPool, HashSetPool, ObjectPool, and a few others. All of these derive from the base class ObjectPool.

Even though ObjectPool is the most complex to use, it's also the most flexible, and the only one that's easy to observe for debugging.

Let's talk about how to declare a pool of List<int>

public ObjectPool<List<int>> m_IntListPool = new ObjectPool<List<int>>(
            () => new List<int>(),
            null, l => l.Clear());

This declaration specifies that the action on create (instantiating a new list) is new List<int>, the null for the second parameter means that there's no action on Get (asking for something from the pool) from the pool, and that the action on Release (returning to the pool) is to clear the list.

There are other parameters but this is a common pattern for this generic class.

If you replaced the 'null' in the declaration with l => l.Add(255) then a Get of a List<int> instance would would have one single element, set to 255. Not too useful in this case, and indeed, for most collections clearing the collection when Releasing the instance makes sense.

As a matter of fact, if the list holds references to other objects instead of ints, it really needs to be cleared, or the instance returned to the pool would still have the object references intact. That's a potential problem!

The bespoke collection pooling classes like ListPool, DictionaryPool, and HashSetPool all clear the collection on release.

Using the Pooler

To get a pooled list you can do one of two things:

var listPool = m_IntListPool.Get()

or if you need the pooled list for a short time, you can do this:

using (m_IntListPool.Get(out var list))
{
     //Do something with the list
}

When the list goes out of scope it's disposed. Dispose in this case just returns the list to the pool.

If you are not using 'using' then here's how to return the list to the pool:

m_IntListPool.Release(that_list);

Here's a property that you can use to get the status of the pool:

public string ListPoolStat => $"All:{m_IntListPool.CountAll.ToString()}, Active:{m_IntListPool.CountActive.ToString()}, Inactive:{m_IntListPool.CountInactive.ToString()}";

This property will show all pooled instances, all active instances, and all inactive instances. Why is this useful? When using the pools you need some way to be able to check if you are returning the pooled object to the pool. If not, you'll have a memory leak.

This becomes important if you try to use the more specific generic versions like ListPool. These have a static reference to the underlying pool, but it's declared internal so you don't have access to CountAll, CountActive, or CountInactive. You can't look at these in a debugger either, at least not easily.

Practical use

Although the simple examples shown here are instance fields, it's better to have these in a static class so that the pool is persistent throughout your app's lifetime.

In a monobehaviour, you can do things like this:

using (ListPool<int>.Get(out var list))
{
     //do something with the list
}

The pool itself is static, so every time you use the pool, lists are obtained from and restored to the same pool, even if you use this construct in different scripts: there's one pool for each Type. Clearly, since the scope of the list is known, you know it's been released to the pool when it's no longer being used.

But if you're using Get to obtain a pooled list and Release to return the list to the pool, you have be careful to pair these operations carefully. Two types of errors can occur:

  • InvalidOperationException if you release an object more than once.
  • Memory leaks if you don't release the object at all.

If you have the pool in a static class you can access it from anywhere and it can be monitored with CountAll, CountActive, and/or CountInactive. The using forms are generally only useful when you have a limited scope.

More Examples

For some good examples, download my free TilePlus Toolkit asset from the Unity Asset Store at u3d.as/2EJR

The file SpawningUtil.cs shows how to pool and spawn prefabs, including preloading.

The next release has more implementations of pooling:

  • TpLib.cs uses pooled Lists and Dictionaries.

  • The file TdDemoGameController.cs method UpdateAgents shows how to use the using statement with a list of tiles.

Using these pools carefully is a great way to reduce the load on the garbage collector.