C# - Improving Array Performance with .NET 7

Unmanaged memory is dangerous, but fast!

.NET 7 introduced a new static class called NativeMemory, which can be used to allocate unmanaged memory.

Normally when working with arrays in C#, the .NET runtime manages the allocation of memory and frees it when it's no longer used, hence the name managed memory.

When using NativeMemory, you're on your own. The .NET garbage collector won't free it when you no longer need it, and the .NET runtime won't perform bounds checks when reading or writing to it. The advantage is it's faster to work with as the bounds checks performed by .NET aren't present.

Warning! Allocating 1024 integers and then writing to the 1025th one will overwrite memory that may be used elsewhere. Be careful!

Usage

Allocate a 1kb block of unmanaged memory:

var byteCount = 1024;
void* data = NativeMemory.Alloc(byteCount);

Allocate and zero a block of unmanaged memory:

void* data = NativeMemory.AllocZeroed(1024);

Resize a block of unmanaged memory to 2kb:

NativeMemory.Realloc(data, 2048);

Free a block of unmanaged memory:

NativeMemory.Free(data);
data = null;

Example - Accumulating an Array

Using managed memory:

// Allocate managed memory
const int LENGTH = 1024;
int[] data = new int[LENGTH];


// Accumulate
int total = 0;

for (int i = 0; i < LENGTH ; i++)
    total += data[i];


// Log the result
Console.WriteLine(total);

Using unmanaged memory:

// Allocate unmanaged memory
const int LENGTH = 1024;
int byteCount = LENGTH * sizeof(int);
int* data = (int*)NativeMemory.Alloc(byteCount);


// Option 1
// - Same syntax as the managed example
int total = 0;

for (int i = 0; i < LENGTH; i++)
    total += data[i];


// Option 2
// - Different syntax, same functionality
for (int i = 0; i < LENGTH; i++)
    total += *(data + i);


// Option 3 - 
// - Make a copy of the data pointer, read from it, then increment it
// - This works because pointers are essentially a 32 or 64 bit
//   integer, meaning we can increment and compare them
var readPtr = data;
var endPtr = data + LENGTH;

while (readPtr < endPtr)
{
    total += *readPtr;
    readPtr++;
}


// Log the result
Console.WriteLine(total);

Benchmarks

The above managed and unmanaged examples were benchmarked using BenchmarkDotNet, with an array length of 32768.

Time required to accumulate a managed array vs an unmanaged array. 12.538us average time for managed, 9.616us average time for unmanaged.