Deep dive: The design and implementation of object pools

July 15, 2024
None

This post first appeared on our blog at Gamelogic.

In software development, particularly in game development and real-time applications, efficient memory management is crucial for maintaining optimal performance. One common technique employed to manage objects efficiently is known as object pooling.

An object pool is a design pattern that pre-allocates a set of objects and keeps them ready for use, rather than creating and destroying objects on the fly. This pool of objects can be reused, significantly reducing the overhead of object creation and garbage collection.

If you are a game programmer, you may have seen the pattern described in Robert Bystrom’s Game Design Patterns. His description and implementation are more relevant to languages such as C++.

Here we look at the design and implementation of pools with a focus on C## and Unity. We will also look at some basic variations, and how the design changes if we require that the onbjects implement an interface. We look at some design techniques that come up in the implementation of pools, and finally, I give what I think is a set of best practices when it comes to the implementation of pools.

The problem

Here is more specifically the problems we can solve with an object pool:

  • Repeatedly obtaining some resource (such as new objects or network connections) creates performance problems.

  • Allocating and deallocating lots of objects causes garbage collection pauses.

  • The number of resources of a certain type is limited, or you want to limit them. The number of simultaneous environment sounds is one example.

  • Our memory allocation pattern causes memory fragmentation, which can make it harder to get big blocks of memory.

A first implementation

The easiest way to implement an object pool is to use a stack of inactive objects.

{
    inactiveObjects =  Stack<Bat>();

     ( i = ; i < n; i++)
    {
        inactiveObjects.Push( Bat());
    }
}
() => inactiveObjects.// what  the stack is empty
 Release(obj) => inactiveObjects.we will use the pool like this:

() = ()
// Later...

(

This implementation suffers from a few issues:

  • We need to repeat the three methods whenever we want an object pool. This is especially problematic when we need more than one pool in the same class.

  • We need to slightly modify the stack and methods for different types.

  • We could accidentally modify the stack in a way that is inconsistent with how we use it as a pool.

  • Running out of objects is handled poorly.

  • We need to remember to activate and deactivate objects when getting them from the stack and releasing them.

  • The code becomes messy if we want to implement a pool policy, such as behavior to execute when we do not have enough inactive objects.

A stack based pool

  • We can solve these issues by encapsulating the methods in a class, and adding a few small features. Since we will be introducing more pool types later, here is the common interface they all will implement:

 {
     Capacity { get; }
    bool HasAvailableObject { get; }
     InactiveObjectCount { get; }
    ;
    ;
    ;
    ;
}

And here is our stack-based implementation of this interface:

  StackPool<T> : IPool<T>
{
     readonly Stack<T> inactiveObjects = ();
     readonly Func<T> createActive;
     readonly Action<T> activate;
     readonly Action<T> deactivate;
     readonly Action<T> destroy;

      Capacity { get;  set; }
      InactiveObjectCount => inactiveObjects.;
     bool HasAvailableObject => inactiveObjects. > ;

     StackPool(
         capacity,
        Func<T> createActive,
        Action<T> activate,
        Action<T> deactivate,
        Action<T> destroy)
    {
        Capacity = capacity;
        .createActive = createActive;
        .activate = activate;
        .deactivate = deactivate;
        .destroy = destroy;

        CreateActive(capacity);
    }

      CreateActive( capacity)
    {
         ( i = ; i < capacity; i++)
        {
            var newObject = createActive();
            deactivate(newObject);
            inactiveObjects.(newObject);
        }
    }

     T Get()
    {
         (inactiveObjects. == )
        {
              InvalidOperationException();
        }

        var obj = inactiveObjects.();
        activate(obj);
         obj;
    }

      Release(T obj)
    {
        deactivate(obj);
        inactiveObjects.(obj);
    }

      IncreaseCapacity( increment)
    {
        Capacity += increment;
        CreateActive(increment);
    }

      DecreaseCapacity( decrement)
    {
         inactiveDecrement = inactiveObjects. < decrement ? inactiveObjects. : decrement;

         ( i = ; i < inactiveDecrement; i++)
        {
            var obj = inactiveObjects.();
            destroy(obj);
        }

        Capacity -= inactiveDecrement;
         inactiveDecrement;
    }
}

Note the following:

  • It is much easier to use the pool anywhere we need it.

  • The stack cannot be accidentally modified by a user.

  • It is generic, so we can pool objects of different types. To allow this we have to take in the createActive function.

  • The pool automatically activates and deactivates objects as they are being obtained and released. To allow this we have to take activate and deactivate actions.

  • The pool provides a way for the user to check whether there is an object available, and throws an exception when the user tries to get an object but cannot.

  • The pool supports crude resizing. To allow this, we take a destroy action.

For pool users: how to implement the pool delegates

The pool user needs to supply the pool with four delegates. Here are notes on how these delegates should be implemented.

  • createActive: This function should create an active object. The pool will deactivate them after they have been created. If this seems slightly counter-intuitive, note that creation functions (such as Unity’s Instantiate) usually give objects that are active by default, so if we required users to supply inactive objects, they would have to deactivate the objects themselves.

    This function should do any work that does not need to be repeated, including allocating the resources for the class to use. If you create and destroy objects in your activate and deactivate actions, that could work against your pooling strategy — so instead those objects should be created and destroyed in the create and destroy delegates, and made active and inactive as the main object itself.

  • activate: This action should prepare the object for use, for example, make it visible, start coroutines, etc. This action should not create objects. Be careful to not accidentally allocate memory when activating objects (for example, by duplicating a material by setting its color or calling an allocating LINQ method).

  • deactivate: This action should make the object inactive: make it invisible, stop its update from running, stop coroutines, and so on. It should also make any of the objects it controls inactive, especially if they were created or became active after the object itself became active.

    If the object acquired resources that cannot be reused, this action should release them. (If this behavior of your object causes garbage collection pauses, you need to redesign how these resources are managed so they too can be reused instead.)

JikGuard.com, a high-tech security service provider focusing on game protection and anti-cheat, is committed to helping game companies solve the problem of cheats and hacks, and providing deeply integrated encryption protection solutions for games.

Read More>>