RecyclableStreamManager vs MemoryStreams
Hi, in this article, I will discuss the common pitfalls developers encounter while using MemoryStreams and how a little-known library from Microsoft can help address this issue.
Microsoft.IO.RecyclableMemoryStream
Although it was introduced back in 2015 to be used in .NET Framework, this library only started to gain traction in .NET Core which is widely used in small containerized (docker/kubernetes) applications where smaller memory allocation can see issues with memory leaks.
Described as "A library to provide pooling for .NET MemoryStream objects to improve application performance, especially in the area of garbage collection", the focus of the library is to ensure large streams do not tax the Garbage Collector by allocating the objects to the Gen 2 heap, thereby improving performance and reducing overall memory consumption.
Memory Stream
In a typical scenario, the best practice for using MemoryStreams is to dispose it after its usage is complete. This is typically done by wrapping it around a using block.
public byte[] SerializeWithStream()
{
using (var memoryStream = new MemoryStream())
{
JsonSerializer.Serialize(memoryStream, Item);
var bytes = memoryStream.ToArray();
return bytes;
}
}
public BenchmarkItems.Item DeserializeWithStream()
{
using (var memoryStream = new MemoryStream(Bytes))
{
return JsonSerializer.Deserialize<BenchmarkItems.Item>(memoryStream);
}
}
Recyclable Memory Stream
With RecyclableStreams, it's advised to declare the manager as a static or singleton to avoid multiple instances of the manager running at a given time which beats the purpose of having a shared pool to hold the buffers.
private static readonly RecyclableMemoryStreamManager recyclableMemoryStream = new RecyclableMemoryStreamManager();
The actual code looks almost similar to the conventional MemoryStream.
public byte[] BenchmarkWithStreamManager()
{
using (var memoryStream = recyclableMemoryStream.GetStream())
{
JsonSerializer.Serialize(memoryStream, Item);
var bytes = memoryStream.ToArray();
return bytes;
}
}
public BenchmarkItems.Item DeserializeWithStreamManager()
{
using (var memoryStream = recyclableMemoryStream.GetStream(Bytes))
{
return JsonSerializer.Deserialize<BenchmarkItems.Item>(memoryStream);
}
}
Benchmarks
The recyclable stream really shines when dealing with larger objects, for example, a large text object to be serialized/deserialized. For this example, I will be using a 2MB JSON data which under normal circumstances will end up on the Gen2 heap taking up valuable space allocated to the instance.
Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
SerializeWithStream | 8.653 ms | 0.2689 ms | 0.7800 ms | 718.7500 | 687.5000 | 687.5000 | 4.65 MB |
BenchmarkWithStreamManager | 7.500 ms | 0.2607 ms | 0.7521 ms | 171.8750 | 171.8750 | 171.8750 | 1.05 MB |
DeserializeWithStream | 11.803 ms | 0.2345 ms | 0.5098 ms | 234.3750 | 218.7500 | - | 1.41 MB |
DeserializeWithStreamManager | 11.986 ms | 0.3047 ms | 0.8985 ms | 234.3750 | 218.7500 | - | 1.41 MB |
Analysis
As you can see from the above benchmark, using the Recyclable Stream Manager improves performance slightly, but the Gen2 allocation is significantly less than using a standard MemoryStream. The reason we are even seeing this is because the test code uses .ToArray() to quickly return the byte array. In future posts, I will describe how to further optimize this.