Memory<T> and ReadOnlyMemory<T>
Heap-safe counterparts to Span for async and storage scenarios
Memory<T> and ReadOnlyMemory<T>
Master modern .NET memory allocation with free flashcards and spaced repetition practice. This lesson covers Memory<T> and ReadOnlyMemory<T> primitives, Span<T> relationships, and zero-copy techniquesβessential concepts for building high-performance .NET applications that minimize heap allocations and garbage collection pressure.
π» Memory<T> and ReadOnlyMemory<T> are modern allocation primitives introduced in .NET Core 2.1 that represent contiguous regions of memory without requiring that memory to live on the managed heap. They're the "storable" cousins of Span<T> and ReadOnlySpan<T>, designed to work in asynchronous scenarios and as class fields where ref structs cannot.
Welcome to Modern Memory Management
Traditional .NET development relied heavily on arrays and strings, which always allocate on the heap and create garbage collection pressure. When you needed to pass around subsets of data, you'd either copy the data (expensive) or pass indices around (error-prone). Memory<T> and ReadOnlyMemory<T> solve these problems by providing a unified abstraction over different memory sources while supporting zero-copy slicing operations.
πΊ Think of Memory<T> as a "bookmark" to a contiguous region of memory. Just like a bookmark doesn't contain the book's pages but tells you where to find them, Memory<T> doesn't contain the data itselfβit's a lightweight wrapper that points to memory that could be:
- A managed array on the heap
- Native memory allocated outside the GC
- Memory-mapped files
- Stack-allocated memory (when converted from Span<T>)
Core Concepts
What Are Memory<T> and ReadOnlyMemory<T>?
Memory<T> is a value type (struct) that represents a contiguous region of arbitrary memory, similar to ArraySegment<T> but far more powerful and flexible. Its read-only counterpart, ReadOnlyMemory<T>, provides the same capabilities but prevents modification of the underlying data.
<table> <tr><th>Feature</th><th>Memory<T></th><th>ReadOnlyMemory<T></th></tr> <tr><td>Storage location</td><td>Stack (value type)</td><td>Stack (value type)</td></tr> <tr><td>Can be field</td><td>β Yes</td><td>β Yes</td></tr> <tr><td>Async support</td><td>β Yes</td><td>β Yes</td></tr> <tr><td>Modification</td><td>β Mutable</td><td>β Read-only</td></tr> <tr><td>GC tracking</td><td>β Yes (if managed)</td><td>β Yes (if managed)</td></tr> </table>
Key Properties and Methods
Both types expose similar APIs:
Properties:
Length- Number of elements in the memory regionIsEmpty- Returns true if Length is 0Span- Gets a Span<T> or ReadOnlySpan<T> over the memory
Methods:
Slice(start, length)- Creates a new Memory<T> over a portion (zero-copy)CopyTo(Memory<T>)- Copies contents to another memory regionPin()- Returns a MemoryHandle for interop scenariosToArray()- Creates a new array copy of the data
The Relationship with Span<T>
π‘ Critical distinction: Memory<T> can be stored in fields and used in async methods, while Span<T> cannot (it's a ref struct). When you need to actually work with the data, you convert Memory<T> to Span<T> via the .Span property.
<pre> βββββββββββββββββββββββββββββββββββββββββββββββ β MEMORY TYPE HIERARCHY β βββββββββββββββββββββββββββββββββββββββββββββββ
Memory<T> ReadOnlyMemory<T>
(mutable) (immutable)
β β
β .Span property β .Span property
β β
Span<T> ReadOnlySpan<T>
(ref struct) (ref struct)
β β
ββββββββββ¬βββββββββββββββββ
β
β
Actual data manipulation
(indexing, iteration, etc.)
</pre>
π§ Memory device: Think "Memory can be Moved around" (stored anywhere), while "Span is Stuck on the stack" (ref struct limitations).
Creating Memory<T> Instances
There are several ways to create Memory<T> instances:
From arrays:
int[] numbers = { 1, 2, 3, 4, 5 };
Memory<int> memory = numbers; // Implicit conversion
Memory<int> slice = numbers.AsMemory(1, 3); // Elements [1,2,3]
From strings:
string text = "Hello, World!";
ReadOnlyMemory<char> memory = text.AsMemory();
ReadOnlyMemory<char> hello = text.AsMemory(0, 5); // "Hello"
Using MemoryPool<T>:
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(1024);
Memory<byte> memory = owner.Memory;
// Use memory...
// Automatically returned when owner is disposed
From ArrayPool<T>:
byte[] rented = ArrayPool<byte>.Shared.Rent(4096);
Memory<byte> memory = rented.AsMemory(0, 1024);
// Use memory...
ArrayPool<byte>.Shared.Return(rented);
Zero-Copy Slicing Operations
One of the most powerful features is slicingβcreating views over subsets of memory without copying data:
int[] data = { 10, 20, 30, 40, 50, 60, 70, 80 };
Memory<int> memory = data;
// All these create views, NO copying occurs
Memory<int> first4 = memory.Slice(0, 4); // [10,20,30,40]
Memory<int> middle = memory.Slice(2, 4); // [30,40,50,60]
Memory<int> last3 = memory.Slice(5); // [60,70,80]
// Modifications through any slice affect the original
first4.Span[0] = 99;
Console.WriteLine(data[0]); // Outputs: 99
<pre> Original array: [10β20β30β40β50β60β70β80] βββββββββ¬ββββββββ first4 slice (no copy!)
Memory layout visualization: βββββββββββββββββββββββββββββββββββββββββββ β 10 β 20 β 30 β 40 β 50 β 60 β 70 β 80 β β Actual data βββββββββββββββββββββββββββββββββββββββββββ β β β β memory (entire array) memory.Slice(5) β points here ββ first4.Span[0] = 99 modifies this </pre>
Working with ReadOnlyMemory<T>
ReadOnlyMemory<T> is essential for immutable data and API contracts that shouldn't allow modification:
public class DataProcessor
{
private readonly ReadOnlyMemory<byte> _data;
public DataProcessor(byte[] data)
{
// Defensive copy NOT needed - ReadOnlyMemory prevents mutation
_data = data;
}
public int CalculateChecksum()
{
ReadOnlySpan<byte> span = _data.Span;
int checksum = 0;
foreach (byte b in span)
{
checksum ^= b;
}
return checksum;
}
}
π Immutability guarantee: Consumers cannot modify data through ReadOnlyMemory<T> even if the original source is mutable. This makes it perfect for caching, sharing data across threads, or public APIs.
Detailed Examples
Example 1: High-Performance String Parsing
Traditional string parsing creates many temporary strings (garbage). Memory<T> enables zero-allocation parsing:
public static class CsvParser
{
public static List<ReadOnlyMemory<char>> ParseLine(ReadOnlyMemory<char> line)
{
var fields = new List<ReadOnlyMemory<char>>();
int start = 0;
ReadOnlySpan<char> span = line.Span;
for (int i = 0; i < span.Length; i++)
{
if (span[i] == ',')
{
// Slice creates a view - no string allocation!
fields.Add(line.Slice(start, i - start));
start = i + 1;
}
}
// Don't forget the last field
if (start < span.Length)
{
fields.Add(line.Slice(start));
}
return fields;
}
}
// Usage:
string csvLine = "John,Doe,42,Engineer";
ReadOnlyMemory<char> memory = csvLine.AsMemory();
List<ReadOnlyMemory<char>> fields = CsvParser.ParseLine(memory);
// Convert to string only when needed
string firstName = fields[0].ToString(); // "John"
string age = fields[2].ToString(); // "42"
Performance benefit: Traditional Split(',') creates 4 string objects on the heap. This approach creates zero heap allocations until you explicitly call ToString().
Example 2: Async I/O with Memory<T>
Memory<T> shines in async scenarios where Span<T> cannot be used:
public class AsyncFileProcessor
{
private readonly Memory<byte> _buffer;
public AsyncFileProcessor(int bufferSize = 4096)
{
_buffer = new byte[bufferSize];
}
public async Task<int> ReadAndProcessAsync(Stream stream)
{
// Memory<T> works in async methods - Span<T> does NOT!
int bytesRead = await stream.ReadAsync(_buffer);
if (bytesRead == 0)
return 0;
// Now convert to Span for actual processing
Span<byte> dataToProcess = _buffer.Span.Slice(0, bytesRead);
// Process the data
int processedCount = ProcessBytes(dataToProcess);
return processedCount;
}
private int ProcessBytes(Span<byte> data)
{
int count = 0;
for (int i = 0; i < data.Length; i++)
{
if (data[i] > 127) // Example: count non-ASCII bytes
count++;
}
return count;
}
}
β‘ Key insight: Memory<T> bridges the gap between async methods (where ref structs can't live) and high-performance Span<T> operations.
<table> <tr><th>Scenario</th><th>Use This</th><th>Why</th></tr> <tr><td>Async method</td><td>Memory<T></td><td>Can survive await points</td></tr> <tr><td>Synchronous processing</td><td>Span<T></td><td>Slightly faster, stack-only</td></tr> <tr><td>Class field</td><td>Memory<T></td><td>Ref structs can't be fields</td></tr> <tr><td>Local variable</td><td>Either</td><td>Both work; Span<T> slightly preferred</td></tr> </table>
Example 3: Memory Pooling for Reduced GC Pressure
Combining Memory<T> with pooling eliminates allocations entirely:
public class PacketProcessor
{
public async Task ProcessPacketsAsync(NetworkStream stream)
{
// Rent from shared pool instead of allocating
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(2048);
Memory<byte> buffer = owner.Memory;
while (true)
{
// Read packet header
int headerBytes = await stream.ReadAsync(buffer.Slice(0, 4));
if (headerBytes == 0) break;
// Parse packet length from header
int packetLength = BitConverter.ToInt32(buffer.Span.Slice(0, 4));
if (packetLength > buffer.Length - 4)
{
throw new InvalidOperationException("Packet too large");
}
// Read packet body
int bodyBytes = await stream.ReadAsync(
buffer.Slice(4, packetLength));
// Process the complete packet
ProcessPacket(buffer.Span.Slice(0, 4 + packetLength));
}
// Memory automatically returned to pool on dispose
}
private void ProcessPacket(ReadOnlySpan<byte> packet)
{
// Packet processing logic
Console.WriteLine($"Processed packet of {packet.Length} bytes");
}
}
π Performance impact:
- Traditional approach: Allocates array per packet β frequent GC
- Pooled approach: Reuses memory β minimal GC, 2-5x faster throughput
Example 4: Slicing Protocol Messages
Parsing binary protocols becomes elegant with Memory<T>:
public readonly struct HttpHeader
{
public ReadOnlyMemory<char> Name { get; }
public ReadOnlyMemory<char> Value { get; }
public HttpHeader(ReadOnlyMemory<char> name, ReadOnlyMemory<char> value)
{
Name = name;
Value = value;
}
}
public static class HttpParser
{
public static List<HttpHeader> ParseHeaders(ReadOnlyMemory<char> headerBlock)
{
var headers = new List<HttpHeader>();
ReadOnlyMemory<char> remaining = headerBlock;
while (!remaining.IsEmpty)
{
// Find line break
ReadOnlySpan<char> span = remaining.Span;
int lineEnd = span.IndexOf('\n');
if (lineEnd == -1) break;
ReadOnlyMemory<char> line = remaining.Slice(0, lineEnd);
remaining = remaining.Slice(lineEnd + 1);
// Find colon separator
ReadOnlySpan<char> lineSpan = line.Span;
int colonPos = lineSpan.IndexOf(':');
if (colonPos == -1) continue;
// Split into name and value (zero-copy!)
ReadOnlyMemory<char> name = line.Slice(0, colonPos);
ReadOnlyMemory<char> value = line.Slice(colonPos + 1).Trim();
headers.Add(new HttpHeader(name, value));
}
return headers;
}
}
// Usage:
string rawHeaders = "Content-Type: application/json\nContent-Length: 1234\n";
ReadOnlyMemory<char> headerMemory = rawHeaders.AsMemory();
List<HttpHeader> headers = HttpParser.ParseHeaders(headerMemory);
foreach (var header in headers)
{
// Convert to string only when displaying
Console.WriteLine($"{header.Name}: {header.Value}");
}
π― Design principle: Keep data as Memory<T> as long as possible. Only convert to concrete types (strings, arrays) at the boundaries of your system.
Common Mistakes
β οΈ Mistake 1: Holding Memory<T> After Source Is Disposed
// β WRONG - Dangerous!
Memory<byte> GetBuffer()
{
using var owner = MemoryPool<byte>.Shared.Rent(1024);
return owner.Memory; // Memory becomes invalid after method returns!
}
// β
RIGHT - Return the owner or copy the data
IMemoryOwner<byte> GetBuffer()
{
return MemoryPool<byte>.Shared.Rent(1024);
// Caller is responsible for disposal
}
β οΈ Mistake 2: Using Span<T> When You Need Memory<T>
// β WRONG - Won't compile!
public class DataCache
{
private Span<byte> _cachedData; // Error: Can't use ref struct as field
}
// β
RIGHT - Use Memory<T> for fields
public class DataCache
{
private Memory<byte> _cachedData;
public void Cache(byte[] data)
{
_cachedData = data;
}
}
β οΈ Mistake 3: Unnecessary Copying
// β WRONG - Creates unnecessary copy
public void ProcessData(ReadOnlyMemory<char> input)
{
string text = input.ToString(); // Allocates string
char[] chars = text.ToCharArray(); // Another allocation!
// Process chars...
}
// β
RIGHT - Work with Span directly
public void ProcessData(ReadOnlyMemory<char> input)
{
ReadOnlySpan<char> span = input.Span; // Zero allocation
// Process span directly...
}
β οΈ Mistake 4: Forgetting ReadOnly Variants
// β WRONG - Mutable when it should be immutable
public Memory<char> GetProductName()
{
return _productName; // Callers can modify!
}
// β
RIGHT - Use ReadOnly for immutable data
public ReadOnlyMemory<char> GetProductName()
{
return _productName; // Safe from modification
}
β οΈ Mistake 5: Pinning Without Disposal
// β WRONG - Memory handle leaks
public void UseNativeApi(Memory<byte> data)
{
MemoryHandle handle = data.Pin();
// ... use handle.Pointer ...
// Forgot to dispose!
}
// β
RIGHT - Always dispose MemoryHandle
public void UseNativeApi(Memory<byte> data)
{
using MemoryHandle handle = data.Pin();
// ... use handle.Pointer ...
} // Automatically unpinned
Key Takeaways
β Memory<T> vs Span<T>: Use Memory<T> when you need to store in fields, pass to async methods, or keep references long-term. Use Span<T> for immediate, synchronous processing.
β
Zero-copy operations: Slicing with .Slice() creates views over existing memory without copying dataβcritical for high-performance scenarios.
β ReadOnly variants: Use ReadOnlyMemory<T> and ReadOnlySpan<T> to enforce immutability and communicate intent in APIs.
β Pooling integration: Combine Memory<T> with MemoryPool<T> or ArrayPool<T> to eliminate allocations and reduce GC pressure.
β Async compatibility: Memory<T> works seamlessly with async/await, while Span<T> cannot cross await points.
β Conversion pattern: Store as Memory<T> β convert to Span<T> for processing β convert back to concrete types only when necessary.
<div style="border: 2px solid #4fd1c5; border-radius: 8px; padding: 16px; margin: 16px 0; background: rgba(79, 209, 197, 0.1);"> <h4>π Quick Reference Card</h4> <table> <tr><td><strong>Type</strong></td><td><strong>Storage</strong></td><td><strong>Async</strong></td><td><strong>Mutable</strong></td></tr> <tr><td>Memory<T></td><td>Field/Property</td><td>β Yes</td><td>β Yes</td></tr> <tr><td>ReadOnlyMemory<T></td><td>Field/Property</td><td>β Yes</td><td>β No</td></tr> <tr><td>Span<T></td><td>Stack only</td><td>β No</td><td>β Yes</td></tr> <tr><td>ReadOnlySpan<T></td><td>Stack only</td><td>β No</td><td>β No</td></tr> </table>
<strong>Common conversions:</strong> <ul> <li><code>array.AsMemory()</code> β Memory<T></li> <li><code>memory.Span</code> β Span<T></li> <li><code>memory.Slice(start, length)</code> β sliced Memory<T></li> <li><code>memory.ToArray()</code> β new array copy</li> </ul>
<strong>When to use:</strong> <ul> <li><strong>Memory<T>:</strong> Async methods, class fields, long-lived references</li> <li><strong>Span<T>:</strong> Synchronous processing, maximum performance</li> <li><strong>ReadOnly*:</strong> Immutable data, public APIs, thread-safe sharing</li> </ul> </div>
π Further Study
Microsoft Docs - Memory<T> and Span<T> usage guidelines: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
Stephen Toub's article on Memory<T> and Span<T>: https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay
Adam Sitnik's performance benchmarks: https://adamsitnik.com/Span/
π‘ Pro tip: Use BenchmarkDotNet to measure the performance impact of Memory<T> in your specific scenarios. The benefits are most dramatic in high-throughput applications processing large amounts of data.
π§ Try this: Convert an existing string parsing method to use ReadOnlyMemory<char> instead of string.Split(). Measure the allocation reduction using a memory profilerβyou'll likely see 80%+ reduction in heap allocations!