Created
December 13, 2021 21:57
-
-
Save teo-tsirpanis/db9ae014efcad311a5df87e6ed04bc51 to your computer and use it in GitHub Desktop.
GC-friendly scratch buffers of arbitrary size in C# made easy.
This file contains 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
// 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; | |
} | |
} |
This file contains 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
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