Skip to content

Instantly share code, notes, and snippets.

@teo-tsirpanis
Created December 13, 2021 21:57
Show Gist options
  • Save teo-tsirpanis/db9ae014efcad311a5df87e6ed04bc51 to your computer and use it in GitHub Desktop.
Save teo-tsirpanis/db9ae014efcad311a5df87e6ed04bc51 to your computer and use it in GitHub Desktop.
GC-friendly scratch buffers of arbitrary size in C# made easy.
// Written by Theodore Tsirpanis.
// Licensed under the CC0 license.
// SPDX-License-Identifier: CC0-1.0
using System.Buffers;
#nullable enable
/// <summary>
/// Manages temporary buffers with low GC pressure.
/// </summary>
/// <remarks>
/// Objects of this type should not be passed around; instead pass their <see cref="Span"/> property.
/// </remarks>
/// <typeparam name="T">The type of items this scratch buffer stores.</typeparam>
internal ref struct ScratchBuffer<T>
{
public Span<T> Span { get; private set; }
private T[]? _arrayToReturn;
/// <summary>
/// Creates a <see cref="ScratchBuffer{T}"/> of length at least <paramref name="minimumLength"/>.
/// </summary>
/// <param name="minimumLength">The buffer's minimum length.</param>
/// <param name="preAllocatedSpan">A pre-allocated <see cref="Span{T}"/> that will
/// be preferred if it has at least <paramref name="minimumLength"/> elements.</param>
/// <remarks>
/// <paramref name="preAllocatedSpan"/> could be allocated from the stack using
/// the <see langword="stackalloc"/> keyword. If it is too small, <see cref="Span"/>
/// will come from the array pool.</remarks>
/// <seealso cref="ScratchBuffer{T}(Int32)"/>
public ScratchBuffer(int minimumLength, Span<T> preAllocatedSpan)
{
if (preAllocatedSpan.Length >= minimumLength)
{
Span = preAllocatedSpan;
_arrayToReturn = null;
}
else
this = new ScratchBuffer<T>(minimumLength);
}
/// <summary>
/// Creates a <see cref="ScratchBuffer{T}"/> of length at least <paramref name="minimumLength"/>.
/// </summary>
/// <param name="minimumLength">The buffer's minimum length.</param>
/// <remarks>The created instance's <see cref="Span"/>
/// will always come from the array pool.</remarks>
public ScratchBuffer(int minimumLength)
{
Span = _arrayToReturn = ArrayPool<T>.Shared.Rent(minimumLength);
}
/// <summary>
/// Disposes the <see cref="ScratchBuffer{T}"/>.
/// </summary>
/// <remarks>
/// This function must be always called after using a <see cref="ScratchBuffer{T}"/>
/// to release any pooled arrays that might have been used. You can use <see langword="using"/>
/// keyword to do it more intuitively. After disposal, the spans returned by the <see cref="Span"/>
/// property must not be used, and accessing it will return an empty span.</remarks>
public void Dispose()
{
if (_arrayToReturn == null) return;
Span = default;
ArrayPool<T>.Shared.Return(_arrayToReturn);
_arrayToReturn = null;
}
}
public static class ScratchBufferDemo
{
public static void Main()
{
// The first argument can be variable but the stackalloc's size
// should better be a small constant for performance and simplicity.
using var stackAllocatedBuffer = new ScratchBuffer<int>(5, stackalloc int[32]);
// Because we asked for 5 thousand integers while giving only thirty
// two from the stack, ScratchBuffer will rent an array from the array pool.
using (var pooledBuffer = new ScratchBuffer<int>(5000, stackalloc int[32]))
{
// We can freely play with the scratch buffer's span, but we must not use it after its
// owning buffer gets out of scope, nor should we pass the ScratchBuffer as a parameter.
// The span might be bigger that what we asked for, just like the array pool.
pooledBuffer.Span.Fill(59);
}
// pooledBuffer is now Disposed, and the pooled array is returned. Keep in mind however that the
// span we stackalloced is not returned even if we didn't use it. You still shouldn't use stackalloc
// inside loops. Consider reusing it if that's the case.
// And last, if we don't pass a span, ScratchBuffer will always rent a pooled array.
// This is especially useful when we want a buffer of a non-unmanaged type.
using var objectBuffer = new ScratchBuffer<object>(100);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment