Effective memory management is a crucial aspect of programming languages, especially when performance and efficiency are paramount. In C#, developers have access to a powerful API, Memory<T>, enabling them to work flexibly and efficiently with memory. In this article, we will delve deep into Memory<T>, exploring its features, advantages, ownership models, and practical usage scenarios. 

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Using Memory<T> in C#

We use a Span to provide a memory-safe representation of a contiguous memory region. Like Span<T>, Memory<T> represents a contiguous memory region. However, it can reside on the managed heap as well as stack instead of just the stack like Span. A Span is a ref struct stored in the stack with some limitations. The compiler will notify us if we incorrectly use a span or any other ref struct.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Now that we have an understanding of Memory<T>, let us see how to create it:

var numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var memory = new Memory<int>(numbers);

We initialize an integer array, and from that array, we create a new instance of Memory<int> type.

Now, let’s see other use cases of Memory<T>.

Allocate Memory<T> on Stack and Heap

We can allocate Memory<T> on both the stack and the heap, unlike Span<T>, which is restricted to the stack:

public static void WorksWithBothStackAndHeap()
{
    Span<int> stackSpan = stackalloc int[3];
    stackSpan[0] = 1;
    stackSpan[1] = 2;
    stackSpan[2] = 3;

    var stackMemory = stackSpan.ToArray().AsMemory();
    var heapArray = new[] { 4, 5, 6 };
    var heapMemory = heapArray.AsMemory();

    Console.WriteLine("Stack Memory:");
    foreach (var item in stackMemory.Span)
    {
        Console.WriteLine(item);
    }

    Console.WriteLine("\nHeap Memory:");
    foreach (var item in heapMemory.Span)
    {
        Console.WriteLine(item);
    }
}

First, we create stackSpan, which is a Span<T> that is allocated on the stack using stackalloc.

Then, we convert it to an array with ToArray() method and create a Memory<T> from it with AsMemory() method. This results in stackMemory being a Memory<T> that represents the same data as stackSpan but is allocated on the heap because arrays in .NET are always heap-allocated.

We define heapArray as an array that we allocate on the heap, and create heapMemory using the AsMemory() method. This results in heapMemory being a Memory<T> that represents the same data as heapArray and is also allocated on the heap.

Finally, we display the contents of both stackMemory and heapMemory.

Memory<T> in C# With Async Methods

Now, let’s use Memory<T> in asynchronous code:

public static async Task ProcessMemoryAsync(Memory<int> memory)
{
    await Task.Delay(1000);
    for (var index = 0; index < memory.Span.Length; index++)
    {
        var item = memory.Span[index];

        Console.WriteLine(item);
    }
}

Here, we simulate an asynchronous operation with Task.Delay() and then we’re accessing the data in memory with memory.Span and printing it to the console. This would not be possible with Span<T> because we cannot use Span<T> in asynchronous code due to being a ref struct.

AsMemory() Extension Method

The extension method String.AsMemory() allows us to create a Memory<char> object from a string without copying the underlying data. This can be useful when passing substrings to methods that accept Memory<T> parameters without incurring additional memory allocations:

public static void StringAsMemoryExtensionMethod()
{
    const string str = "Hello Code Maze";
    var memory = str.AsMemory();
    var slice = memory.Slice(6, 8);

    Console.WriteLine(slice.ToString()); // "Code Maze"
}

First, we create a Memory<char> from a string using the AsMemory() extension method. Then, we slice this Memory<char> to get a new Memory<char> representing a portion of the original string. Finally, we display this slice by converting it to a string using the ToString() method.

Next, let’s focus on advanced techniques in memory management.

Ownership Models and IMemoryOwner Interface

The MemoryPool<T>.Rent() method returns the IMemoryOwner<T> interface, which acts as an owner of a memory block. The shared pool allows for the renting of the memory block. When the memory is no longer in use, the block’s owner is responsible for disposing of it. Let’s see this with the example of how to use IMemoryOwner<T> and MemoryPool<T>:

public static void UseMemoryOwner()
{
    using IMemoryOwner<int> owner = MemoryPool<int>.Shared.Rent(10);
    var memory = owner.Memory;

    for (var i = 0; i < memory.Length; i++)
    {
        memory.Span[i] = i;
    }
    foreach (var item in memory.Span)
    {
        Console.WriteLine(item);
    }
}

First, we rent a memory block from the shared pool with MemoryPool<int>.Shared.Rent() method. This returns an IMemoryOwner<int> that owns the rented block of memory.

Next, we retrieve a Memory<int> that represents this memory block with the owner.Memory property. We use this Memory<int> to store some data, and then display this data.

Finally, we dispose of the IMemoryOwner<int> with the using statement. This returns the block of memory to the pool, making it available for subsequent Rent() calls.

Now that we have a comprehensive understanding of Memory<T>, let’s see a real-world use case of file I/O operations using Memory<T>.

Practical Scenario For Using Memory<T> in C#

We can use MemoryPool<T>, IMemoryOwner<T>, and the IMemoryOwner.Memory property in a practical scenario.

In this case, we’ll create a method that reads data from a file into a rented block of memory, processes the data, and then returns the memory to the pool:

public static async Task ProcessFileAsync(string filePath)
{
    using var owner = MemoryPool<byte>.Shared.Rent(4096);
    Memory<byte> buffer = owner.Memory.Slice(0, 4096);
    await using FileStream stream = File.OpenRead(filePath);

    int bytesRead;
    while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
    {
        var data = buffer.Slice(0, bytesRead);
        for (var index = 0; index < data.Span.Length; index++)
        {
            var b = data.Span[index];
            Console.Write((char)b);
        }

        Console.WriteLine();
    }
}

To begin, in the ProcessFileAsync() method, we obtain a block of memory from the shared pool by calling the MemoryPool<byte>.Shared.Rent() method. This method returns an IMemoryOwner<byte> interface that owns the block of memory.

Consequently, we retrieve a Memory<byte> object that represents the block of memory using the owner.Memory property. Then, we use this Memory<byte> object as a buffer to read data from a file with the FileStream.ReadAsync() method. Once we have read the data, we process it by printing it to the console.

Finally, we dispose of the IMemoryOwner<byte> interface with the using statement in the ProcessFileAsync() method. This action returns the block of memory to the pool, making it available for subsequent Rent() calls.

Conclusion

Developers prioritizing performance and efficiency in their applications can benefit significantly from Memory<T>, a flexible and robust API for managing memory in C#. With the ability to be allocated on both the stack and the heap, interoperability with strings, and ownership models, Memory<T> is a valuable asset. By understanding and leveraging Memory<T>, C# developers can optimize memory usage, improve application performance, and build more robust and scalable software solutions. 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!