In my last post I grumbled about ObjectPool being a separate package. That was essentially the single downside to use it. So, how hard is to implement our own StringBuilder pool?
Well, not that hard. The whole thing can be something like this:
internal static class StringBuilderPool {
private static readonly ConcurrentQueue<StringBuilder> Pool = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static StringBuilder Get() {
return Pool.TryDequeue(out StringBuilder? sb) ? sb : new StringBuilder(4096);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Return(StringBuilder sb) {
sb.Length = 0;
Pool.Enqueue(sb);
return true;
}
}
In our Get
method we check if we have any stored StringBuilder. If yes, we just return the same. If no, we create a new instance.
In the Return
method we just add the returned instance to the queue.
Now, this is not exactly an ObjectPool equivalent. For example, it doesn’t limit the pool size. And it will keep large objects around forever. However, for my case it was good enough and unlikely to cause any problems.
And performance… Well, performance is promising, to say the least:
Test | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|
StringBuilder (small) | 15.762 ns | 0.3650 ns | 0.4057 ns | 0.0181 | - | 152 B |
ObjectPool (small) | 17.257 ns | 0.0616 ns | 0.0576 ns | 0.0057 | - | 48 B |
Custom pool (small) | 16.864 ns | 0.0192 ns | 0.0150 ns | 0.0057 | - | 48 B |
Concatenation (small) | 9.716 ns | 0.1634 ns | 0.1528 ns | 0.0105 | - | 88 B |
StringBuilder (medium) | 58.125 ns | 0.6429 ns | 0.6013 ns | 0.0526 | - | 440 B |
ObjectPool (medium) | 23.226 ns | 0.0517 ns | 0.0484 ns | 0.0115 | - | 96 B |
Custom pool (medium) | 23.660 ns | 0.2515 ns | 0.1963 ns | 0.0115 | - | 96 B |
Concatenation (medium) | 66.353 ns | 1.3307 ns | 1.2447 ns | 0.0793 | - | 664 B |
StringBuilder (large) | 190.293 ns | 0.7781 ns | 0.6498 ns | 0.2496 | 0.0010 | 2088 B |
ObjectPool (large) | 92.556 ns | 0.9281 ns | 0.8228 ns | 0.0755 | - | 632 B |
Custom pool (large) | 91.470 ns | 0.5478 ns | 0.5124 ns | 0.0755 | - | 632 B |
Concatenation (large) | 1,430.599 ns | 11.5971 ns | 10.8479 ns | 4.0169 | 0.0057 | 33600 B |
Pretty much its on-par with ObjectPool implementation. Honestly, results are close enough to be equivalent for all practical purposes.
So, if you don’t want to pull the whole Microsoft.Extensions.ObjectPool just for caching a few StringBuilder
instances, consider rolling your own.