microsoft/Microsoft.IO.RecyclableMemoryStream
C#
Captured source
source ↗microsoft/Microsoft.IO.RecyclableMemoryStream
Description: A library to provide pooling for .NET MemoryStream objects to improve application performance.
Language: C#
License: MIT
Stars: 2135
Forks: 210
Open issues: 13
Created: 2015-01-26T01:29:12Z
Pushed: 2026-06-10T07:54:13Z
Default branch: master
Fork: no
Archived: no
README:
A library to provide pooling for .NET MemoryStream objects to improve application performance, especially in the area of garbage collection.
Get Started
Install the latest version from NuGet
Install-Package Microsoft.IO.RecyclableMemoryStream
Purpose
Microsoft.IO.RecyclableMemoryStream is a MemoryStream replacement that offers superior behavior for performance-critical systems. In particular it is optimized to do the following:
- Eliminate Large Object Heap allocations by using pooled buffers
- Incur far fewer gen 2 GCs, and spend far less time paused due to GC
- Avoid memory leaks by having a bounded pool size
- Avoid memory fragmentation
- Allow for multiple ways to read and write data that will avoid extraneous allocations
- Provide excellent debuggability and logging
- Provide metrics for performance tracking
Features
- The semantics are close to the original
System.IO.MemoryStreamimplementation, and is intended to be a drop-in replacement as much as possible. - Rather than pooling the streams themselves, the underlying buffers are pooled. This allows you to use the simple
Disposepattern to release the buffers back to the pool, as well as detect invalid usage patterns (such as reusing a stream after it’s been disposed). RecyclableMemoryStreamManageris thread-safe (streams themselves are inherently NOT thread safe).- Implementation of
IBufferWrite. - Support for enormous streams through abstracted buffer chaining.
- Extensive support for newer memory-related types like
Span,ReadOnlySpan,ReadOnlySequence, andMemory. - Each stream can be tagged with an identifying string that is used in logging - helpful when finding bugs and memory leaks relating to incorrect pool use.
- Debug features like recording the call stack of the stream allocation to track down pool leaks
- Maximum free pool size to handle spikes in usage without using too much memory.
- Flexible and adjustable limits to the pooling algorithm.
- Metrics tracking and events so that you can see the impact on the system.
Build Targets
At least MSBuild 16.8 is required to build the code. You get this with Visual Studio 2019.
Supported build targets in v2.0 are: net462, netstandard2.0, netstandard2.1, and netcoreapp2.1 (net40, net45, net46 and netstandard1.4 were deprecated). Starting with v2.1, net5.0 target has been added.
Testing
A minimum of .NET 5.0 is required for executing the unit tests. Requirements:
- NUnit test adapter (VS Extension)
- Be sure to set the default processor architecture for tests to x64 (or the giant allocation test will fail)
Benchmark tests
The results are available here
Change Log
Read the change log here.
How It Works
RecyclableMemoryStream improves GC performance by ensuring that the larger buffers used for the streams are put into the gen 2 heap and stay there forever. This should cause full collections to happen less frequently. If you pick buffer sizes above 85,000 bytes, then you will ensure these are placed on the large object heap, which is touched even less frequently by the garbage collector.
The RecyclableMemoryStreamManager class maintains two separate pools of objects:
1. Small Pool - Holds small buffers (of configurable size). Used by default for all normal read/write operations. Multiple small buffers are chained together in the RecyclableMemoryStream class and abstracted into a single stream. 2. Large Pool - Holds large buffers, which are only used when you must have a single, contiguous buffer, such as when you plan to call GetBuffer(). The GetBuffer() method is still bounded by .NET array size limits. If you need a larger stream than this, do not use this method.
A RecyclableMemoryStream starts out by using a small buffer, chaining additional ones as the stream capacity grows. Should you ever call GetBuffer() and the length is greater than a single small buffer's capacity, then the small buffers are converted to a single large buffer. You can also request a stream with an initial capacity; if that capacity is larger than the small pool block size, multiple blocks will be chained unless you call an overload with asContiguousBuffer set to true, in which case a single large buffer will be assigned from the start. If you request a capacity larger than the maximum poolable size, you will still get a stream back, but the buffers will not be pooled. (Note: This is not referring to the maximum array size. You can limit the poolable buffer sizes in RecyclableMemoryStreamManager)
There are two versions of the large pool:
- Linear (default) - You specify a multiple and a maximum size, and an array of buffers, from size (1 * multiple), (2 * multiple), (3 * multiple), ... maximum is created. For example, if you specify a multiple of 1 MB and maximum size of 8 MB, then you will have an array of length 8. The first slot will contain 1 MB buffers, the second slot 2 MB buffers, and so on.
- Exponential - Instead of linearly growing, the buffers double in size for each slot. For example, if you specify a multiple of 256KB, and a maximum size of 8 MB, you will have an array of length 6, the slots containing buffers of size 256KB, 512KB, 1MB, 2MB, 4MB, and 8MB.
Which one should you use? That depends on your usage pattern. If you have an unpredictable large buffer size, perhaps the linear one will be more suitable. If you know that a longer stream length is unlikely, but you may have a lot of streams in the smaller size, picking the exponential version could lead to less overall memory usage (which was the reason this form was added).
Buffers are created, on demand, the first time they are requested and nothing suitable already exists…
Excerpt shown — open the source for the full document.