To Pool or Not to Pool
For a project of mine I “had” to do a lot of string concatenations. Easy solution was just to have a string builder and go wild. But I wondered, does it make sense to use ObjectPool
(found in Microsoft.Extensions.ObjectPool package). Thus, I decided to do a few benchmarks.
For my use case, “small” was just appending 3 items to a StringBuilder. The “medium” is does total of 21 appends. And finally, “large” does 201 appends. And no, there is no real reason why I used those exact numbers other than loop ended up being nice. :)
After all this, benchmark results (courtesy of BenchmarkDotNet):
Test | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|
StringBuilder (small) | 16.295 ns | 0.1240 ns | 0.1160 ns | 0.0181 | - | 152 B |
StringBuilder Pool (small) | 17.958 ns | 0.3125 ns | 0.2609 ns | 0.0057 | - | 48 B |
StringBuilder (medium) | 87.052 ns | 1.5177 ns | 1.4197 ns | 0.0832 | 0.0001 | 696 B |
StringBuilder Pool (medium) | 31.245 ns | 0.1815 ns | 0.1417 ns | 0.0181 | - | 152 B |
StringBuilder (large) | 304.724 ns | 1.6736 ns | 1.3975 ns | 0.4520 | 0.0029 | 3784 B |
StringBuilder Pool (large) | 172.615 ns | 1.5325 ns | 1.4335 ns | 0.1471 | - | 1232 B |
As you can see, if you are doing just a few appends, it’s probably not worth messing with ObjectPool
. Not that you should use StringBuilder
either. If you are adding 4 or fewer strings, you might as well concatenate them - it’s actually more performant.
However, if you are adding 5 or more strings together, pool is no worse than instantiating a new StringBuilder. So, for pretty much any scenario where you would use StringBuilder, it pays off to pool it.
Is there a situation where you would avoid pool? Well, performance-wise, I would say probably no. I ran multiple tests and, on my computer, there was no situation where StringBuilder alone was better than either pool or concat. Yes, StringBuilder is performant at low number of appends, but string concatenation is better. As soon as you go over a few appends, ObjectPool actually makes sense.
However, an elephant in the room is ObjectPool’s dependency on external package. Call me old fashioned but there is a value in not depending on extra packages.
The final decision is, of course, dependant on you. But, if performance is important, I see no reason why not to use ObjectPool. I only wish it wasn’t an extra package.
For curious ones, code was as follows:
[Benchmark]
public string Large_WithoutPool() {
var sb = new StringBuilder();
sb.Append("Hello");
for (var i = 0; i < 100; i++) {
sb.Append(' ');
sb.Append("World");
}
return sb.ToString();
}
[Benchmark]
public string Large_WithPool() {
var sb = StringBuilderPool.Get();
try {
sb.Append("Hello");
for (var i = 0; i < 100; i++) {
sb.Append(' ');
sb.Append("World");
}
return sb.ToString();
} finally {
sb.Length = 0;
StringBuilderPool.Return(sb);
}
}
And yes, I also tested just a simple string concatenation (quite optimized for smaller number of concatenations):
Test | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|
Concatenation (small) | 9.820 ns | 0.2365 ns | 0.2429 ns | 0.0105 | - | 88 B |
Concatenation (medium) | 146.901 ns | 1.6561 ns | 1.2930 ns | 0.2294 | - | 1920 B |
Concatenation (large) | 4,710.573 ns | 43.5370 ns | 96.4750 ns | 15.2054 | 0.0458 | 127200 B |