• Frugal Cafe
  • Posts
  • FC36: Let's beat ValueStringBuilder in CPU

FC36: Let's beat ValueStringBuilder in CPU

Reused large StringBuilder is more efficient both in memory and CPU

In previous post (FC35: .Net Core ValueStringBuilder, not designed for large data (beehiiv.com)) we discussed the new ValueStringBuilder introduced in .Net Core is using much more memory than StringBuilder when forming large strings. To generate a little over 1 million characters string, it needs to use 8 mb of memory (in char[] buffers in array pool), 4x that of StringBuilder; and there are multiple data copying between those buffers.

So let’s try to beat ValueStringBuilder both in memory and CPU, using large StringBuilder pool. Here is a simple StringBuilder pool implemented using ConcurrentQueue:

There is no rejection of large StringBuilder on returning, so the builders can grow as large as the scenarios need them to be. Of course, you can add rejection and trimming logic if you want.

Now we can change string.Join to be:

Perf test:

This code is comparing with string.Join in .Net Core implemented using ValueStringBuilder. Test results:

Output strings are 1,213,701. Allocation sizes are basically the same in two cases: the builders are all using reused buffers.

The StringBuilder base replacement is 36% faster.

Notice the capacity for the reused StringBuilder is 1,213,701 characters, just 2,491 characters more than string length. This is due to StringBuilder growth pattern when adding small chunks of data: maximum chunk size is 8,000 characters. So StringBuilder based solution is using much less memory, than ValueStringBuilder base solution, and have less data copying. Reused StringBuilder basically is a single StringBuilder with a single char[] array.

It’s also much easier to control your own StringBuilder pool. The default array pool has automatically trimming logic, controlled by memory pressure. So user code has no control over it.