Last active
January 22, 2024 11:05
-
-
Save neon-sunset/a2e42c5c60eeaaa0e3709a1230bcf39a to your computer and use it in GitHub Desktop.
Tiered buffer allocation example: stackalloc -> array pool -> native memory alloc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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