Skip to content

Instantly share code, notes, and snippets.

@neon-sunset
Last active January 22, 2024 11:05
Show Gist options
  • Save neon-sunset/a2e42c5c60eeaaa0e3709a1230bcf39a to your computer and use it in GitHub Desktop.
Save neon-sunset/a2e42c5c60eeaaa0e3709a1230bcf39a to your computer and use it in GitHub Desktop.
Tiered buffer allocation example: stackalloc -> array pool -> native memory alloc
using Cysharp.Collections; // dotnet add package NativeMemoryArray
namespace BufferAllocExample;
public static class ExampleBufferedBytesProcessor
{
public static int ProcessBytes(IUtf8Processor processor, ReadOnlySpan<char> source, Span<byte> destination)
{
const int StackAllocLimit = 1024; // 1KB
const int ArrayPoolLimit = 1024 * 1024 * 10; // 10MB
byte[]? pooledArray = null;
NativeMemoryArray<byte>? nativeMemoryArray = null;
var sourceByteCount = Encoding.UTF8.GetByteCount(source);
var sourceBytes = sourceByteCount switch
{
<= StackAllocLimit => stackalloc byte[StackAllocLimit], // 1KB
<= ArrayPoolLimit => pooledArray = ArrayExtensions.RentPooled(sourceByteCount), // 10MB
// If you have this much data, consider streaming with processing chunks instead.
// Otherwise, it's fast and will deterministically deallocate without fragmenting runtime heap.
// Perfect for large transient allocations.
_ => (nativeMemoryArray = ArrayExtensions.AllocNative(sourceByteCount)).AsSpan()
};
// Slice the span to the desired length.
// It's best to not stackalloc with variable size, use const length + slice instead.
// Also, current ArrayPool<T>.Shared impl. always uses arrays with length rounded up to pow2.
sourceBytes = sourceBytes[..sourceByteCount];
_ = Encoding.UTF8.GetBytes(source, sourceBytes);
var processedByteCount = processor.ProcessBytes(sourceBytes, destination);
// If we crash before reaching this, it is acceptable to not Free/Return because ArrayPool<T>.Shared
// will just allocate a new array if it doesn't already have a one with >= requested length,
// and the runtime will simply collect a not-returned array. This behavior might even be desirable
// because in unsafe code we can actually corrupt the rented array, and returning it will be a terrible idea.
// When it comes to native memory, it will be eventually freed via NativeMemoryArray's finalizer.
pooledArray?.Return();
nativeMemoryArray?.Free();
return processedByteCount;
}
}
internal static class ArrayExtensions
{
public static byte[] RentPooled(int minimumLength) => ArrayPool<byte>.Shared.Rent(minimumLength);
public static void Return(this byte[] pooledArray) => ArrayPool<byte>.Shared.Return(pooledArray);
// addMemoryPressure: true confuses runtime and messes with its memory usage heuristics
public static NativeMemoryArray<byte> AllocNative(int length) => new(length, skipZeroClear: true, addMemoryPressure: false);
public static void Free(this NativeMemoryArray<byte> nativeArray) => nativeArray.Dispose();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment