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.

MethodMeanErrorStdDevGen0Gen1Gen2Allocated
SerializeWithStream8.653 ms0.2689 ms0.7800 ms718.7500687.5000687.50004.65 MB
BenchmarkWithStreamManager7.500 ms0.2607 ms0.7521 ms171.8750171.8750171.87501.05 MB
DeserializeWithStream11.803 ms0.2345 ms0.5098 ms234.3750218.7500-1.41 MB
DeserializeWithStreamManager11.986 ms0.3047 ms0.8985 ms234.3750218.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.

Did you find this article valuable?

Support Devindran Ramadass by becoming a sponsor. Any amount is appreciated!