Allocating memory in C# is relatively expensive, and is a key point of optimization for any performance-critical application. Object Pooling is one technique that can help reduce the overhead of a memory-intensive application.
How Does Object Pooling Improve Performance?
At the end of the day, the performance of computers is basically limited to two things: CPU processing speed and memory performance. The former can be improved with more efficient algorithms that use less clock cycles, but often it’s harder to optimize memory usage, especially when working with large datasets and very short timescales.
Object Pooling is a technique used to reduce memory allocations. There’s often no getting around needing to allocate memory, but you maight not need to allocate as often as you do. Object allocations are slow for a few reasons—the underlying memory is allocated on the heap (which is a lot slower than value-type stack allocations), and complex objects can have constructors that are performance-intensive. Plus, because it’s heap-based memory, the garbage collector will need to clean it up, which can hurt performance if you’re triggering it too often.
For example, say you have a loop that runs many times, and allocates a new object like a List for every execution. This is a lot of memory being used, and it isn’t getting cleaned up until after it all finishes and garbage collection runs. The following code will run 10,000 times, and leave 10,000 ownerless lists allocated in memory at the end of the function.
When the garbage collector eventually runs, it’s going to have a very hard time cleaning up all this junk, which will negatively impact performance while waiting for GC to finish.
Instead, a more sensible approach is to initialize it once, and reuse the object. This makes sure that you’re reusing the same space in memory, rather than forgetting about it and letting the garbage collector deal with it. It’s not magic, and you’re going to have to clean up eventually.
For this example, the recyclable approach would be running new List before the loop to do the first allocation, and then running .Clear or resetting the data to save on memory space and garbage created. After this loop finishes, there will only be one list left in memory, which is a lot smaller than 10,000 of them.
Object Pooling is basically a generic implementation of this concept. It’s a collection of objects that can be reused. There’s no official interface for one, but in general they have an internal data store and implement two methods: GetObject(), and ReleaseObject().
Rather than allocating a new object, you request one from the object pool. The pool may create a new object if it doesn’t have one available. Then, when you’re done with it, you release that object back to the pool. Rather than chucking the object in the garbage, the object pool keeps it allocated but clears it from all data. The next time you run GetObject, it returns the new empty object. For large objects, like Lists, doing this is much easier on the underlying memory.
You do need to make sure you’re releasing the object before using it again, because most pools will have a maximum number of empty objects that they keep on hand. If you try to get 1,000 objects from the pool, you’ll dry it up, and it will start allocating them normally upon request, which defeats the purpose of a pool.
In performance-critical cases, especially when working often with lots of repeated data, object pooling can dramatically improve performance. However, it’s not a catchall, and there are plenty of times that you would not want to pool objects. Allocations with new are still quite fast, so unless you’re allocating large chunks of memory very often, or allocating objects with performance heavy constructors, it’s often better to just use new rather than pooling unnecessarily, especially considering that the pool itself adds extra overhead and complexity to the allocation. Allocating a single regular object will always be faster than allocating a single-pooled object; the performance difference comes when you’re allocating many times.
You’ll also need to make sure your pooling implementation properly clears object state. If not, you can risk GetObject returning something that has stale data. This could be a massive problem if, for example, you returned a different user’s data when fetching someone else. Generally, it’s not an issue if done properly, but it’s something to keep in mind.
If you want to use Object Pools, Microsoft offers an implementation in Microsoft.Extensions.ObjectPool. If you want to implement it yourself, you can open up the source to check out how it works in DefaultObjectPool. Generally, you’ll have a different object pool for each type, with a maximum number of objects to keep in the pool.