9 minutes to read
Created by
Calytic
Updated by
Wulf

Pooling

Reducing allocations and garbage collections with pooling

This guide is for uMod, not Oxide.
Join our discord for the latest updates and the latest news! Join discord

A pool provides a repository of active and ready-made objects that may be obtained upon request and freed back to the pool after they are no longer needed.

The purpose of pooling is to reduce memory footprint and the number of cycles normally associated with constructing and destroying objects.

Normally when an object is instantiated, memory must be allocated for that object and when the object is no longer needed it is marked for destruction. An object marked for destruction it is not destroyed immediately, instead it sits in memory until the garbage collector destroys the object later.

var objectArray = new object[2];
var anotherObjectArray = new object[2];

The above code will create two object arrays which must be garbage collected. In contrast, the following code will only create one object array and is not garbage collected.

var objectArray = ArrayPool.Get(2);
ArrayPool.Free(objectArray);
var anotherObjectArray = ArrayPool.Get(2);
ArrayPool.Free(anotherObjectArray);

In the case where a program must create a lot of objects, the garbage collector may in-fact represent a significant and typically unavoidable performance bottleneck. Pooling provides a relatively easy way around this bottleneck by avoiding memory allocations and garbage collections altogether.

Array pooling

An array pool provides a simple and cheap way to obtain an array that has very little overhead. It is not appropriate for every array implementation, but is still extremely powerful especially in cases where a lot of arrays must be created.

Object array pool

uMod provides a simple and versatile ArrayPool for pooling an array of loosely typed objects.

// Obtain an object array with a size of 2
object[] myArray = uMod.Pooling.ArrayPool.Get(2);

myArray[0] = true;
myArray[1] = "hello";

// Later, after array is no longer needed
uMod.Pooling.ArrayPool.Free(myArray);

The largest array that may be pooled, by default, can have 50 elements. Larger arrays must use a custom strongly-typed ArrayPool (documented below) or a conventional array implementation.

String Array Pool

In addition to the object array pool documented above, uMod also provides a StringPool.

// Obtain an string array with a size of 2
string[] myArray = uMod.Pooling.StringPool.Get(2);

myArray[0] = "hello";
myArray[1] = "world";

// Later, after array is no longer needed
uMod.Pooling.StringPool.Free(myArray);

Strongly-Typed Array Pool

While the above object and string array pooling is certainly useful, sometimes a strongly-typed pool is needed for other types and custom abstractions.

// Create a strongly typed integer array pool
uMod.Pooling.ArrayPool<int> myPool = new uMod.Pooling.ArrayPool<int>();

// Obtain an integer array with a size of 2
int[] myArray = myPool.Get(2);

myArray[0] = 1;
myArray[1] = 2;

// Later, after array is no longer needed
myPool.Free(myArray);

Using try / finally

It is important to ensure that when an object or array is obtained from a pool that it is freed back to that (same) pool. This can be difficult when exceptions are thrown by any code in-between.

If there is any chance that an exception may be thrown, wrap the code with a "try/finally" block to ensure the code within the "finally" is always executed no matter the outcome.

// Obtain an object array with a size of 2
object[] myArray = uMod.Pooling.ArrayPool.Get(2);

try
{
  throw new System.Exception("Oh no!");
  return myArray;
}
finally
{
  uMod.Pooling.ArrayPool.Free(myArray);
}

Dynamic object pooling

uMod provides a dynamic object pool which is used to pool frequently used objects of any type. Like the array pooling documented above, the dynamic object pool reduces memory allocations and the size/frequency of garbage collections.

Several built-in dynamic object pools are provided by default, but any abstraction may be pooled using dynamic object pooling.

Generic Pooling

For custom abstractions, the default dynamic pool provides a simple way to leverage the benefits of pooling.

class MyClass
{
  public string MyProperty;
}

// Create a pool for MyClass
uMod.Pooling.DynamicPool<MyClass> myPool = uMod.Pooling.Pools.Default<MyClass>();

// Obtain an object of type MyClass from pool
MyClass myObject = myPool.Get();

myObject.MyProperty = "Hello, World";

// Later, after the object is no longer needed
myPool.Free(myObject);

Reset

When a generic object is freed back to the pool it will retain the state information of the object. When the object is used again, it's internal state must be overwritten or the application may have unexpected behavior.

One way to do this is to implement a Reset method which is similar to a constructor.

class MyClass
{
  public string MyProperty;
  public bool MyBool;
  
  public void Reset(string myProperty)
  {
      MyProperty = myProperty;
      MyBool = false;
  }
}

// Create a pool for MyClass
uMod.Pooling.DynamicPool<MyClass> myPool = uMod.Pooling.Pools.Default<MyClass>();

// Obtain an object of type MyClass from pool
MyClass myObject = myPool.Get().Reset("Hello World");

// Later, after the object is no longer needed
myPool.Free(myObject);

IDisposable

As an alternative to the above Reset implementation, a generic object may implement the System.IDisposable interface in combination with the Disposable pool.

class MyClass : System.IDisposable
{
  public string MyProperty;
  public bool MyBool;
  
  public void Dispose()
  {
    MyProperty = string.Empty;
    MyBool = false;
  }
}

// Create a pool for MyClass
uMod.Pooling.DynamicPool<MyClass> myPool = uMod.Pooling.Pools.Disposable<MyClass>();

// Obtain an object of type MyClass from pool
MyClass myObject = myPool.Get();

myObject.MyProperty = "Hello, World";

// Later, after the object is no longer needed
myPool.Free(myObject);

When the above object is freed back to the disposable pool, the Dispose method is called automatically. The state information from the previous iteration will not be kept when the object is re-used.

List pooling

The List<T> type is a type which is used quite often without regard for the inherent performance implications and thus pooling lists represents an easy and common way to make an application more performant.

The built-in List<T> pool follows a similar pattern compared to the array pooling implementation documented above.

// Obtain a List<int> from the pool
List<int> myList = uMod.Pooling.Pools.GetList<int>();

myList.Add(1);
myList.Add(2);

// Later, after the List<int> is no longer needed
Pools.FreeList(ref myList);

Queue pooling

Much like List<T> pooling, Queue<T> instances may be pooled in a similar way.

// Obtain a Queue<int> from the pool
Queue<int> myQueue = uMod.Pooling.Pools.GetQueue<int>();

myQueue.Enqueue(1);
myQueue.Enqueue(2);

// Later, after the Queue<int> is no longer needed
uMod.Pooling.Pools.FreeQueue(ref myQueue);

StringBuilder pooling

Use the StringBuilderPool to re-use StringBuilders.

// Obtain a StringBuilder from the global pool
uMod.Text.StringBuilder myStringBuilder = uMod.Pooling.Pools.StringBuilderPool.Get();

myStringBuilder.Append("Hello, World");

// Later, after the StringBuilder is no longer needed
uMod.Pooling.Pools.StringBuilderPool.Free(myStringBuilder);