The most misunderstood design pattern – Object Pool

Have you ever heard about Object Pooling? If you are a software developer then probably you have. It is usually described as a design pattern that allows you to decrease memory allocations. Is this all Object Pooling is? Well, not even close.

The basics

To talk about properties of the Object Pool pattern let’s first take a look at what it actually is in code. Based on the MSDN generic the Object Pool implementation for a multithreaded application may look as follows:

public class ObjectPool
{
    private readonly ConcurrentBag _items = new ConcurrentBag();
    private readonly Func _generator;

    public ObjectPool(Func generator)
    {
        _generator = generator;
    }

    public T Get()
    {
        T item;
        return _items.TryTake(out item) ? item : _generator();
    }

    public void Put(T item)
    {
        _items.Add(item);
    }
}

We have here:

  • ConcurrentBag class that should resolve any multithread-related problems with multiple threads simultaneous access to one collection.
  • Methods that will make it possible to claim and return an instance of the desired class,
  • A generic class that can be reused for multiple applications.

Actual Purpose

When using the Object Pool pattern you declare one thing and one thing only: “I, as a developer, know how lifetime of these objects should be handled better than garbage collector.” And if you actually do, that is fine. If you are not 100% sure about where to use the Object Pool, here are some indicators:

  • Make sure that the cost of creating an instance of the class you desire to pool is much higher than resetting some of its internal properties.
  • The frequency of creating the target class is also high.
  • The number of concurrent instances of the target class is relatively small.

If all the above conditions are true you may consider using an Object Pool. Here are some most common usages:

  • Threads – creating a thread is an extremely expensive operation so .NET introduced an internal mechanism called ThreadPool.
  • Database connections – creating a single connection is time-consuming so ADO.NET created SQL Server Connection Pooling.
  • Socket connections – presents the same problem as database connections. The solution was presented in this
  • Large graphics objects – as bitmaps, fonts etc. are also faster to reload than create from scratch.

You may find it interesting that that the Object Pool is widely used in video games. For example many FPS games use this pattern to manage “creating” bullets.

Problems

Everything seems perfect until you start using the Object Pool everywhere. Some pitfalls become clearly visible then:

  • You have to reset the pooled object state manually. They are not hard to implement but every one of them carries some drawbacks. There are several ways to automatize it:
    • use a common interface for every pooled type that will enforce implementing the Reset method.
    • pass the reset method to the Object Pool that will perform an operation on each pooled object
  • Inadequate resetting object may lead to information leak or random and unreproducible exceptions.
  • The pool may waste memory on unneeded objects because in most cases it does not reduce the number of stored objects.
  • Code becomes more complicated. It is not a big deal but it is noticeable.
  • Some resources may expire and still be artificially kept alive by the pool.
  • Some experts point out that multithread synchronization of the pool may be a problem. But ConcurrentBag solves almost all of those problems out of the box.
  • A limited number of items in the pool. If the pool has a number limit – some threads may be forced to wait for their items. On the other hand if the pool has an unlimited size – it may lead to allocating enormous amounts of memory without releasing them later on.

Performance

Let’s consider this class:

class CustomClass
{
    public int[] Data { get; set; }

    public CustomClass(int size)
    {
        Data = new int[size];
        Random rand = new Random();
        for (int i = 0; i < Data.Length; i++)
        {
            Data[i] = rand.Next();
        }
    }
}

It has an array of Integers that is initialized in its constructor. If the “Size” parameter in the constructor is relatively small this class will be fast to create. The larger the value of “size” becomes the longer it will take this class to initialize. I have created a series of tests that would measure performance difference expressed in time that is required to create this class when using the Object Pool pattern and when the class is created by using a “new” keyword. My assumptions:

  • The class will be used in a loop and will be needed for each loop iteration. Number of loop iterations is expressed by the “Repeats” column in the table below.
  • The class will receive different “size” constructor parameter. Value of this parameter is expressed by the “Size” column in the table below.
  • Both Object Pool and a normal object creation will be given identical “Size” and “Repeats” values for each test.
  • Time will be measured by the “Stopwatch” class and the “ElapsedTicks” property.
  • Time that the Object Pool pattern needed to complete the loop iteration is expressed by the “Pool Ticks” column in the table below.
  • Time that was needed to complete the loop iteration with the “new” keyword is expressed by the “Normal Ticks” column in the table below.
  • The test will be run in the parallel loop using the “Parallel.For” method.
  • The final performance benchmark will be given as percentage. This is expressed by column “Performance Difference” in the table below.

After running series of tests I received following statistics:

Size Repeats Normal Ticks Pool Ticks Performace Diffrence
10 10 104 47 221.2  %
10 100 471 173 272.2 %
10 1000 3871 1152 336.0 %
100 10 176 55 320.0 %
100 100 930 179 537.5 %
100 1000 8244 1165 707.6 %
1000 10 778 64 1215.6  %
1000 100 6316 177 3 568.3 %
1000 1000 56931 1443 3 945.3 %

Although we have to remember that these are just approximate numbers (due to operating system limitations and background processes), there is clear performance boost with increasing size of integer array as well as number of loops. It is mainly a result of reusing the same class instances over and over again. For example, Object Pool has usually created between 6-8 instances even in 1000 parallel loops. This is a result of how Parallel.For is implemented – it uses tasks to get maximum CPU performance without trying to overload it with more parallel tasks than it can handle simultaneously.

Conclusion

Object pool can bring great benefits or be a disaster in you project. It all depends on an individual situation. If you think of using it please take two things into account:

  • Check if Object Pool will actually significantly increase performance of your solution. If the performance increase is not very high – it is better not to use this pattern.
  • Check if there already is a library/framework that you can reuse in your application. Someone has probably spent a significant amount of time doing it the right way.

Tags: , , , ,