In my last post, The hidden cost of mpsc channels, I discussed a common pitfall with Rust mpsc channels using more memory than intuitive (and subsequently got roasted by people pretending they read the footnotes of the documentation of every transitive dependency they use ๐).
I recently stumbled upon a similar surprising cause of memory utilization in Go, this time in sync.Pool.
sync.Pool's "purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector."
Notice the goal is not to reduce memory usage, but to reduce allocations.
However, without being familiar with the implementation details, it's easy to assume that it doesn't increase memory usage much.
While the docs say "The store scales under load (when many goroutines are actively printing) and shrinks when quiescent.", the surprise for me was how it scales.
I had assumed it would scale with the number of concurrent usages - that is, if 5 callers had called Get() without a Put(), we could have up to 5 objects.
The surprise for me was that the memory usage was not just scaling with the amount of concurrent usages, but with the number of processors (GOMAXPROCS).
On my machine defaulting to GOMAXPROCS=32, that means each pool could hold up to 32 copies of an object even without any concurrent usage.
This comes from sync.Pool using processor-local local pools (see this post for a great explanation) in order to improve throughput.
For most cases, this is a win. However, in the use case I was investigating, I was seeing a mostly-idle application with 10mb of memory being held by Prometheus's gzip compressor, which uses sync.Pool. Because this was a mostly idle application, GC ran infrequently, meaning we would quickly grow to filling up each processor-local pool with its own expensive gzip state.
After turning off compression (and thus no longer using sync.Pool for the gzip compressor), the memory usage dropped significantly:
So overall, while sync.Pool is often a great tool, it's certainly not always a great choice.
If you have large objects with little or no concurrent usage, consider alternatives like sharing a single instance behind a mutex.