What is an Object Pool?
An Object Pool is a collection of pre-initialized objects, ready for use on-demand. In many cases, pooling is much more efficient than allocating and deallocating memory each time a new instance of an object is needed. When an object is needed from a pool, it is taken off of a reserve list, and placed onto an active list.
> Object Pools are sometimes seen taking advantage of the <a href="https://www.devmaking.com/learn/design-patterns/singleton-pattern/" target="_blank" style="color:inherit;">Singleton Pattern</a> to allow for fast, static access.
Object Pools can also grow dynamically in the case that all of the reserve objects are in use. Additionally, some implementations give them growth capacities, to limit the amount of memory it is allowed to use.
> Managing database connections and preventing out of memory errors are two cases where you might consider limiting the size of an object pool.
<div style="width:100%; margin:auto;text-align:center;"> <img src="https://www.devmaking.com/img/topics/designpatterns/ObjectPoolPattern_01.png" alt="object pool UML diagram" style="width:650px;max-width:95%;"> </div>
Key Components:
- Client: The object that asks for a reusable object from the ObjectPool.
- ObjectPool: manages the active and reserved objects.
- Reusable: the type of object managed by the Object Pool that can be recycled.
The Cost of Allocating Memory
Whenever you create an object using the new
keyword, the program needs to ask for more memory from the computer behind the scenes. When you delete
an object, or when it falls out of scope in languages that have Garbage Collection, the program lets the computer know it isn't using the memory at that memory address anymore, allowing it to be used by other processes.
While this process keeps your computer from running out of RAM, it takes time to allocate and deallocate memory, and in high-performance situations, this can create bottlenecks that slow down the system when too many objects are being created or deleted all at once, especially if those objects are complex and need some time to initialize.
> If you ever play a video game and notice the framerate drop when a lot happens all at once on the screen, it's possible that inefficient memory management could have played a role.
With an Object Pool, memory is allocated once when the pool is initialized<sup>1</sup>, and that memory is recycled by the program. This makes Object Pools ideal for situations where there are many similar objects being frequently used, such as particle effects like rain or snow, or bullets in FPS games.
<div style="width:100%; margin:auto;text-align:center;"> <img src="https://www.devmaking.com/img/topics/designpatterns/ObjectPoolPattern_02.png" alt="allocations with and without object pools" style="width:600px;max-width:95%;"> </div>
Additionally, Object Pools can help prevent Garbage Collection in languages such as Java and C#. While modern GC has made many improvements, it can still cause unexpected lags in high-performance systems.
> <sup>1</sup>If the pool runs out of reserve objects, more can be created. Luckily, this only occurs when a new peak usage occurs. As the developer, the initial reserve can be designed so that needing to add more objects is rare. > > In some cases, you may need to design the Object Pool to work with a hard limit, and have the program handle a situation where all of the resources are being used.
Conceptualization
To show how Object Pools work, we'll implement a pool that manages GameObjects. To focus on the concept, we'll just work with the basic properties that a 3D GameObject might have; position, rotation, and scale.
<div style="width:100%; margin:auto;text-align:center;"> <img src="https://www.devmaking.com/img/topics/designpatterns/ObjectPoolPattern_03.png" alt="gameObject pool UML diagram" style="width:650px;max-width:95%;"> </div>
Modeling the GameObject
Focusing on the GameObject for a moment, the position, rotation, and scale of the game object in the game world can all be expressed as components with x, y, z
properties in 3D space. To capture this in our GameObejct class, we'll express them three components as Vectors.
class Vector3D
{
public float x;
public float y;
public float z;
/*...*/
public Vector3D()
{
x = 0f;
y = 0f;
z = 0f;
}
}
> Note that in a real 3D game development setting, we might want to express rotation in terms of Quaternions. However, a simple vector will suffice for this concept.
With our Vector3D
class defined, we can now model our GameObject class with some basic properties.
class GameObject
{
public Vector3D position;
public Vector3D rotation;
public Vector3D scale;
/*...*/
public bool isActive;
// Constructor
public GameObject()
{
position = new Vector3D();
rotation = new Vector3D();
scale = new Vector3D();
isActive = false;
}
public void SetActive(bool state)
{
this.isActive = state;
}
//...
}
Notice that the class also contains an isActive
property. This can help the game engine determine whether the GameObject should be visible or active in the game.
Another thing you might have noticed is that we need to new
all of the vector properties. When we create a GameObject, we're also creating three vectors. With four total new
operations to complete on this relatively simple object, imagine how costly more complex objects might be to create! This is why Object Pools are necessary in situations that require hundreds or even thousands of comlpex objects at any given moment.
Modeling the Object Pool
Object Pools primarily focus on managing two lists: an active list, and a reserve list. The active list is made up of objects currently checked out by other parts of the program, and the reserve list is composted of objects that are ready to be put into action.
While I reccomend implementing your own container class to manage the object pool, to keep things simple in this example, we'll use a List<T>
implementation. For reference, the list class will be able to do the following actions:
class List<T>
{
public void Add(T item);
public boolean Remove(T item);
public T GetFirst();
}
> Note that this is language agnostic: List<T> can represent any container class or list implementation, and is not specific to any programming language.
Having defined the list class we'll be able to take advantage of, now we can model the basic properties of the GameObject pool:
class GameObjectPool
{
// Our active and reserve lists:
List<GameObject> activeList;
List<GameObject> reserveList;
int numberActive;
int numberReserved;
// ...
}
While not necessary, we also keep track of the number of GameObjects that are active, and the number being reserved. The reserve list can actually help us determine if we need to grow the reserve list!
We're not done with the GameObject pool just yet; before we finish creating the object pool, we need to come up with a way to recycle our GameObject's so they can be used like-new every time.
Recycling GameObjects
In order to sucessfully recycle the GameObjects, we need a way to reset the GameObjects before setting them back active again. This helps prevent the object's previous state from interferring with the new state. We also need a way to set the new state of the GameObject. To accomplish this, we'll create some methods in our Vector3D
and GameObject
classes.
In our vector class, we'll implement a Clean
method that effectively resets its properties. We'll also add a Set
method for setting each property:
class Vector3D
{
//...
public void Clean()
{
this.x = 0f;
this.y = 0f;
this.z = 0f;
}
public void Set(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
//...
}
With our vector class done, now let's do the same to the GameObject class. This time, instead of a single Set
method, though, we'll create separate methods for each property:
class GameObject
{
//...
public void Clean()
{
this.position.Clean();
this.rotation.Clean();
this.scale.Clean();
}
public void SetPosition(float x, float y, float z)
{
this.position.Set(x,y,z);
}
public void SetRotation(float x, float y, float z)
{
this.rotation.Set(x,y,z);
}
public void SetScale(float x, float y, float z)
{
this.scale.Set(x,y,z);
}
//...
}
Our GameObject and vector classes are all set up to be recycled by the pool now, so let's finish it up!
Creating The Object Pool
Now that everything is set up, it's time to implement our object pool!
Initializing The Reserve
When we want to initialize our object pool, we'll want to have a few GameObjects in the reserve list ready for use. Depending on how you want to design your solution, you could allow the number to be passed in as a parameter, or have it as a constant value. In this implementation, we'll pass a parameter with a default value:
class GameObjectPool
{
//...
public GameObjectPool(int reserve = 5)
{
// Default values:
this.activeList = new List<GameObject>();
this.reserveList = new List<GameObject>();
this.numberActive = 0;
this.numberReserved = 0;
// Make sure that we were given a legal reserve value:
Assert(reserve >= 0);
// Initialize the reserve:
InitializeReserve(reserve);
}
private void InitializeReserve(int reserveSize)
{
for( int i = 0; i < reserveSize; i++ )
{
// Create a new GameObject:
GameObject gameObject = new GameObject();
// Add it to the reserve list:
this.reserveList.Add(gameObject);
// Increment the number of reserved.
this.numberReserved++;
}
}
//...
}
Getting GameObjects from the Reserve
Now that we have our reserve list initialized with a few GameObjects, let's define how we can retrieve them:
class GameObjectPool
{
//...
public GameObject GetGameObject()
{
// If we're out of reserve objects..
if( this.numberReserved == 0 )
{
// ..grow the reserve:
GameObject tmp = new GameObject();
this.reserveList.Add(tmp);
this.numberReserved++;
}
// Get the first object off of the reserve:
GameObject gameObject = this.reserveList.GetFirst();
// Remove it from the reserve:
this.reserveList.Remove(gameObject);
this.numberReserved--;
// Add it to the active list:
this.activeList.Add(gameObject);
this.numberActive++;
// Important part: reset the node to be like new!
gameObject.Clean();
return gameObject
}
//...
}
When getting GameObjects from the reserve, it's important to make sure that the reserve isn't empty. If we're out of reserve objects, we need to add new objects to the reserve. This doesn't have to be just 1 new GameObject, though; we can define a GrowthSize
variable that determines how much the reserve should grow every time we run out of reserve GameObjects.
Once a GameObject has been retrieved from the reserve list, we add it to the active list, and finally Clean
the GameObject to set it back to the state it was in when we created it.
Disposing GameObjects
So far, we have initialized a reserve and allowed for GameObjects to be retrieved from the reserve list. Now we need to implement the ability for nodes to be returned to the reserve when the program is done using them. This is key to making sure the objects are being properly recycled!
class GameObjectPool
{
//...
public void ReturnGameObject(GameObject gameObject)
{
// Assert that we were given a GameObject and not null:
Assert(gameObject != null);
// Remove the GameObject from the active list:
this.activeList.Remove(gameObject)
this.numberActive--;
// Add it back into the reserve list:
this.reserveList.Add(gameObject);
this.numberReserved++;
}
// ...
}
> If you need to be extra cautious, you can check that the GameObject was actually in the list before executing the methtod, but that isn't entirely necessary in all situations.
You might have noticed that the Clean
method was used when retrieving a GameObject from the reserve, and not when pushing it back into the reserve. In most cases, it doesn't matter if you clean the GameObject when returning it or when Getting it, as long as Clean
is called at least once between the time it is done being used and the time it needs to be used again!
Considerations for Using a Singleton
We Implemented the GameObjectPool as a regular class. However, we could have also implemented it using the <a href="https://www.devmaking.com/learn/design-patterns/singleton-pattern/" target="_blank" style="color:inherit;">Singleton Pattern</a> to allow it to act as a static, single-source for managing all of the GameObjects in the program. This involves making the constructor private, favoring a Create
method to start using the class, and making all of the methods static
. Additionally, all of the static methods would need to reference the singleton instance in order to work on the active and reserve lists.
> Challenge: Implement the GameObjectPool as a Singleton!
The Drawback of Object Pools
Now that we've implemented an Object Pool, we can discuss some of the drawbacks of using them. Object pools are often criticized for leaving unused memory sitting around, which can be limiting when memory resources are especially low. This makes sense; after all, when we have a reserve built up, that's just memory waiting to be used, but it's reserved for only one specific use!
Ultimately it is up to you, the designer, to balance the needs of the project. If you need high-performance, then ask yourself how much more you need the speed than a little extra memory at your disposal. If you are on a tight memory budget, ask youself if you really need the extra speed, or see if you can balance out the two by implementing a reserve capacity.
In either case, always make sure to weigh your options when designing a system!
Object Pool Vs Flyweight
Object Pools can sometimes be confused with the <a href="https://www.devmaking.com/learn/design-patterns/flyweight-pattern/" target="_blank" style="color:inherit;">Flyweight Pattern</a>, since both deal with managing memory and have performance benefits. The key difference comes down to intention.
In an Object Pool, objects are reused and recycled, so a GameObject can be repurposed over and over again. Contrastingly, when you add an object to a Flyweight, that object is referenced by other objects, possibly even pooled objects!
For example, think of a video game scene of a rainy grassland. There are many rain droplets that fall and dissapear when they hit the ground, which makes them a great candidate for an Object Pool.
These rain droplets all need an image for the player to see, though. Since the image is the same for every single rain droplet, it makes sense to have all of the rain drops reference the same raindrop image, so we only have one image of a rain droplet loaded into the scene for all of the droplets to use. For this, we would use a Flyweight to maintain the raindrop image!
Recommended Resources
- <a target="_blank" href="https://www.amazon.com/gp/product/0596007124/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596007124&linkCode=as2&tag=devmaking-20&linkId=bc32087110669f75d93b216df79816f0" style="color:#fff;border-radius:3px;background-color:#888;padding:1px 5px">Head First Design Patterns: A Brain-Friendly Guide</a> : An excellent primer for learning design patterns in a pragmatic way.
- <a target="_blank" href="https://www.amazon.com/gp/product/0201633612/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0201633612&linkCode=as2&tag=devmaking-20&linkId=ae25d94c4ea49870eb3115e0e4d2de90" style="color:#fff;border-radius:3px;background-color:#888;padding:1px 5px">Design Patterns: Elements of Reusable Object-Oriented Software</a> : The reference guide made famous by Gang of Four, still widely used today.
- <a href="https://www.draw.io" target="_blank" style="color:#fff;border-radius:3px;background-color:#888;padding:1px 5px">Draw.io</a>: A free, open-source tool for designing diagrams with built-in support for UML diagrams to make your own!