dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.07k stars 4.69k forks source link

[API Proposal]: High-performance, low-allocation convenience "writer" for Memory<T> #91728

Closed adam-dot-cohen closed 1 year ago

adam-dot-cohen commented 1 year ago

Background and motivation

High-performance, low-allocation, heap-based convience type for constructing Memory structures - implementing IBufferWriter / IMemoryOwner. And manages arbitrarry backing ArrayPool rental length on behalf of user.

API Proposal

using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Buffers;

/// <summary>
/// Represents a heap-based, array-backed output sink into which <typeparamref name="T"/> data can be written.
/// </summary>
/// <typeparam name="T">The type of items to write to the current instance.</typeparam>
/// <remarks>
public class MemoryBufferWriter<T> : IBufferWriter<T>, IMemoryOwner<T>
{
    /// <summary>
    /// The default size to use to expand the buffer.
    /// </summary>
    private const int DefaultGrowthIncrement = 512;

    /// <summary>
    /// Array on current rental from the array pool.  Reference to the same memory as <see cref="_buffer"/>.
    /// </summary>
    private T[] _array;

    /// <summary>
    /// The increment to use to grow the writer.
    /// </summary>
    /// 
    private readonly int _growthIncrement;

    /// <summary>
    /// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
    /// </summary>
    private Memory<T> _buffer;

    /// <summary>
    /// The current position of the writer.
    /// </summary>
    private int _index;

    /// <summary>
    /// The disposed state of the buffer.
    /// </summary>
    private bool _disposed;

    /// <summary>
    /// Initializes a new instance of the <see cref="MemoryBufferWriter{T}"/> class.
    /// </summary>
    public MemoryBufferWriter()
        : this(ArrayPool<T>.Shared, DefaultGrowthIncrement)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MemoryBufferWriter{T}"/> class.
    /// </summary>
    /// <param name="growthIncrement">The incremental size to grow the buffer.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="growthIncrement"/> is not valid.</exception>
    public MemoryBufferWriter(int growthIncrement)
        : this(ArrayPool<T>.Shared, growthIncrement)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MemoryBufferWriter{T}"/> class.
    /// </summary>
    /// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
    /// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="growthGrowthIncrement"/> is not valid.</exception>
    public MemoryBufferWriter(ArrayPool<T> pool, int growthGrowthIncrement)
    {
        if (growthGrowthIncrement < 1)
            throw new ArgumentOutOfRangeException("The growth increment parameter bust be greater than 0");

        this._buffer = this._array = ArrayPool<T>.Shared.Rent(growthGrowthIncrement);
        this._growthIncrement = growthGrowthIncrement;
        this._index = 0;
        this._disposed = false;
    }

    /// <summary>
    /// Gets the data written to the underlying buffer so far as a <see cref="ReadOnlyMemory{T}"/>.
    /// </summary>
    public ReadOnlyMemory<T> WrittenMemory
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get
        {
            return this._buffer.Slice(0, this._index);
        }
    }

    /// <summary>
    /// Gets the data written to the underlying buffer so far as a <see cref="ReadOnlySpan{T}"/>.
    /// </summary>
    public ReadOnlySpan<T> WrittenSpan
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        get
        {
            return this._buffer.Span.Slice(0, this._index);
        }
    }

    /// <inheritdoc />
    Memory<T> IMemoryOwner<T>.Memory => this._buffer;

    /// <inheritdoc />
    public void Advance(int count)
    {
        if(this._index + count <= this._buffer.Span.Length)
            this.Grow(count);

        this._index += count;
    }

    /// <inheritdoc />
    public Memory<T> GetMemory(int sizeHint = 0)
    {
        if (sizeHint == 0)
            sizeHint = 8;

        this.Grow(sizeHint);

        var slcIndex = this._index;

        this._index += sizeHint;

        return this._buffer.Slice(slcIndex, sizeHint);
    }

    /// <inheritdoc />
    public Span<T> GetSpan(int sizeHint = 0)
    {
        if (sizeHint == 0)
            sizeHint = 8;

        this.Grow(sizeHint);

        var slcIndex = this._index;

        this._index += sizeHint;

        return this._buffer.Span.Slice(slcIndex, sizeHint);
    }

    /// <summary>
    /// Appends to the end of the buffer, automatically growing the buffer if necessary.
    /// </summary>
    /// <param name="items">A <see cref="Span{T}"/> of items to append.</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Write(Span<T> items)
        => this.Copy(items);

    /// <summary>
    /// Appends to the end of the buffer, automatically growing the buffer if necessary.
    /// </summary>
    /// <param name="items">Array of items to append.</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Write(T[] items)
        => this.Copy(items);

    /// <summary>
    /// Appends to the end of the buffer, automatically growing the buffer if necessary.
    /// </summary>
    /// <param name="items">A <see cref="Memory{T}"/> of items to append.</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Write(Memory<T> items)
        => this.Copy(items.Span);

    /// <summary>
    /// Appends a single item to the end of the buffer, automatically growing the buffer if necessary.
    /// </summary>
    /// <param name="item"> Item <see cref="T"/> to append.</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Write(T item)
        => this.Copy(item);

    /// <summary>
    /// Appends to the end of the buffer, automatically growing the buffer if necessary.
    /// </summary>
    /// <param name="items">A <see cref="Span{T}"/> of items to append.</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ValueTask WriteAsync(Memory<T> items)
    {
        this.Copy(items.Span);

        return ValueTask.CompletedTask;
    }

    /// <summary>
    /// Appends a single item to the end of the buffer, automatically growing the buffer if necessary.
    /// </summary>
    /// <param name="items">A <see cref="Span{T}"/> of items to append.</param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ValueTask WriteAsync(T item)
    {
        this.Copy(item);

        return ValueTask.CompletedTask;
    }

    /// <summary>
    /// Grows the buffer if needed.
    /// </summary>
    /// <param name="length"></param>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void Grow(int length)
    {
        if (this._index + length <= this._buffer.Span.Length) return;

        var next = ArrayPool<T>.Shared.Rent(Math.Max(this._index + this._growthIncrement, this._index + length));

        this._buffer.Span.CopyTo(next);

        ArrayPool<T>.Shared.Return(this._array);

        this._buffer = this._array = next;
    }

    /// <summary>
    /// Returns a slice of the underlying buffer
    /// </summary>
    /// <param name="length">The length of the desired slice.</param>
    /// <returns><see cref="Span{T}"/> of the underlying buffer.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void Copy(Span<T> items)
    {
        this.Grow(items.Length);

        var slc = this._buffer.Span.Slice(this._index, items.Length);

        items.CopyTo(slc);

        this._index += items.Length;

    }
    /// <summary>
    /// Returns a slice of the underlying buffer
    /// </summary>
    /// <param name="length">The length of the desired slice.</param>
    /// <returns><see cref="Span{T}"/> of the underlying buffer.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void Copy(T item)
    {
        this.Grow(1);

        var slc = this._buffer.Span.Slice(this._index, 1);

        slc[0] = item;

        this._index += 1;
    }

    /// <inheritdoc />
    public void Dispose()
    {
        ArrayPool<T>.Shared.Return(this._array);

        this._buffer = null;
    }
}

API Usage

// count of sample integers we'll append the writer
var cnt = 2000;

//1. INSTANTIATE
var writer = new MemoryBufferWriter<int>();

//2. Write single entries
for (int i = 0; i < cnt; i++)
{
    // write to SpanBufferWriter
    writer.Write(i);
}

//3. Write array, span or memory...
writer.Write(span);

//4. Read contents from - `WrittenSpan` OR `WrittenMemory`
writer.WrittenSpan;
writer.WrittenMemory;
Method TotalCount Mean Error Allocated
'Proposed MemoryBufferWriter' 100 2.204 us 0.1077 us 992 B
'High Perf Toolkit ArrayPoolWriter' 100 2.258 us 0.0641 us 1104 B
'MS RecyclableMemoryStream' 100 5.747 us 0.2749 us 872 B
'DotNext SparseBufferWriter' 100 6.773 us 0.2598 us 1160 B
'Proposed MemoryBufferWriter' 1000 3.173 us 0.1577 us 992 B
'High Perf Toolkit ArrayPoolWriter' 1000 2.824 us 0.1062 us 2168 B
'MS RecyclableMemoryStream' 1000 6.660 us 0.3037 us 872 B
'DotNext SparseBufferWriter' 1000 7.545 us 0.2564 us 1160 B
'Proposed SpanBufferWriter' 10000 6.066 us 0.1118 us 992 B
'High Perf Toolkit ArrayPoolWriter' 10000 6.643 us 0.1354 us 12872 B
'MS RecyclableMemoryStream' 10000 11.748 us 0.3450 us 872 B
'DotNext SparseBufferWriter' 10000 12.834 us 0.4699 us 1336 B
'Proposed MemoryBufferWriter' 100000 34.320 us 0.4676 us 992 B
'High Perf Toolkit ArrayPoolWriter' 100000 38.629 us 0.6964 us 119744 B
'MS RecyclableMemoryStream' 100000 59.608 us 1.1849 us 872 B
'DotNext SparseBufferWriter' 100000 56.021 us 0.9304 us 3272 B
'Proposed MemoryBufferWriter' 1000000 146.279 us 2.9135 us 992 B
'High Perf Toolkit ArrayPoolWriter' 1000000 180.226 us 1.3876 us 1188488 B
'MS RecyclableMemoryStream' 1000000 520.908 us 2.8181 us 1504 B
'DotNext SparseBufferWriter' 1000000 553.517 us 1.3129 us 22632 B

Alternative Designs

The growth increment could definitely be optimized based upon avg size written via linear growth, exponentiation or logistic regression/additive equation.

Risks

Reference implementation...

https://github.com/adam-dot-cohen/ResizableSpanWriter

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-system-buffers See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation High-performance, low-allocation, heap-based convience type for constructing Span and Memory structures without specifying size - implementing IBufferWriter / IMemoryOwner. Better performance and efficiently than alternatives such as MemoryStream, RecyclableMemoryStream and ArrayPoolBufferWriter (MS High Performance Toolkit). ### API Proposal ```csharp using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace System.Buffers; /// /// Represents a heap-based, array-backed output sink into which data can be written. /// /// The type of items to write to the current instance. /// public class SpanWriter : IBufferWriter, IMemoryOwner { /// /// The default size to use to expand the buffer. /// private const int DefaultGrowthIncrement = 256; /// /// Array on current rental from the array pool. Reference to the same memory as . /// private T[] _array; /// /// The increment to use to grow the writer. /// /// private readonly int _growthIncrement; /// /// The instance used to rent . /// private Memory _buffer; /// /// The current position of the writer. /// private int _index; /// /// The disposed state of the buffer. /// private bool _disposed; /// /// Initializes a new instance of the class. /// public SpanWriter() : this(ArrayPool.Shared, DefaultGrowthIncrement) { } /// /// Initializes a new instance of the class. /// /// The incremental size to grow the buffer. /// Thrown when is not valid. public ResizableSpanWriter(int growthIncrement) : this(ArrayPool.Shared, growthIncrement) { } /// /// Initializes a new instance of the class. /// /// The instance to use. /// The minimum capacity with which to initialize the underlying buffer. /// Thrown when is not valid. public SpanWriter(ArrayPool pool, int growthGrowthIncrement) { if (growthGrowthIncrement < 1) throw new ArgumentOutOfRangeException("The growth increment parameter bust be greater than 0"); this._buffer = this._array = ArrayPool.Shared.Rent(growthGrowthIncrement); this._growthIncrement = growthGrowthIncrement; this._index = 0; this._disposed = false; } /// /// Gets the data written to the underlying buffer so far as a . /// public ReadOnlyMemory WrittenMemory { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return this._buffer.Slice(0, this._index); } } /// /// Gets the data written to the underlying buffer so far as a . /// public ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return this._buffer.Span.Slice(0, this._index); } } /// Memory IMemoryOwner.Memory => this._buffer; /// public void Advance(int count) { if(this._index + count <= this._buffer.Span.Length) this.Grow(count); this._index += count; } /// public Memory GetMemory(int sizeHint = 0) { if (sizeHint == 0) sizeHint = 8; this.Grow(sizeHint); var slcIndex = this._index; this._index += sizeHint; return this._buffer.Slice(slcIndex, sizeHint); } /// public Span GetSpan(int sizeHint = 0) { if (sizeHint == 0) sizeHint = 8; this.Grow(sizeHint); var slcIndex = this._index; this._index += sizeHint; return this._buffer.Span.Slice(slcIndex, sizeHint); } /// /// Appends to the end of the buffer, automatically growing the buffer if necessary. /// /// A of items to append. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Span items) => this.Copy(items); /// /// Appends to the end of the buffer, automatically growing the buffer if necessary. /// /// Array of items to append. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(T[] items) => this.Copy(items); /// /// Appends to the end of the buffer, automatically growing the buffer if necessary. /// /// A of items to append. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Memory items) => this.Copy(items.Span); /// /// Appends a single item to the end of the buffer, automatically growing the buffer if necessary. /// /// Item to append. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(T item) => this.Copy(item); /// /// Appends to the end of the buffer, automatically growing the buffer if necessary. /// /// A of items to append. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueTask WriteAsync(Memory items) { this.Copy(items.Span); return ValueTask.CompletedTask; } /// /// Appends a single item to the end of the buffer, automatically growing the buffer if necessary. /// /// A of items to append. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ValueTask WriteAsync(T item) { this.Copy(item); return ValueTask.CompletedTask; } /// /// Grows the buffer if needed. /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Grow(int length) { if (this._index + length <= this._buffer.Span.Length) return; var next = ArrayPool.Shared.Rent(Math.Max(this._index + this._growthIncrement, this._index + length)); this._buffer.Span.CopyTo(next); ArrayPool.Shared.Return(this._array); this._buffer = this._array = next; } /// /// Returns a slice of the underlying buffer /// /// The length of the desired slice. /// of the underlying buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Copy(Span items) { this.Grow(items.Length); var slc = this._buffer.Span.Slice(this._index, items.Length); items.CopyTo(slc); this._index += items.Length; } /// /// Returns a slice of the underlying buffer /// /// The length of the desired slice. /// of the underlying buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Copy(T item) { this.Grow(1); var slc = this._buffer.Span.Slice(this._index, 1); slc[0] = item; this._index += 1; } /// public void Dispose() { ArrayPool.Shared.Return(this._array); this._buffer = null; } } ``` ### API Usage ```csharp // count of sample integers we'll append the writer var cnt = 2000; //1. INSTANTIATE var writer = new SpanWriter(); //2. Write single entries for (int i = 0; i < cnt; i++) { // write to SpanWriter writer.Write(i); } //3. Write array, span or memory... writer.Write(span); //4. Read contents from - `WrittenSpan` OR `WrittenMemory` Console.WriteLine(writer.WrittenSpan.SequenceEqual(span)); ``` | Method | TotalCount | Mean | Error | Allocated | |------------------------------------ |----------- |-----------:|----------:|----------:| | 'Proposed SpanWriter' | 100 | 2.204 us | 0.1077 us | 992 B | | 'High Perf Toolkit ArrayPoolWriter' | 100 | 2.258 us | 0.0641 us | 1104 B | | 'MS RecyclableMemoryStream' | 100 | 5.747 us | 0.2749 us | 872 B | | 'DotNext SparseBufferWriter' | 100 | 6.773 us | 0.2598 us | 1160 B | | 'Proposed SpanWriter' | 1000 | 3.173 us | 0.1577 us | 992 B | | 'High Perf Toolkit ArrayPoolWriter' | 1000 | 2.824 us | 0.1062 us | 2168 B | | 'MS RecyclableMemoryStream' | 1000 | 6.660 us | 0.3037 us | 872 B | | 'DotNext SparseBufferWriter' | 1000 | 7.545 us | 0.2564 us | 1160 B | | 'Proposed SpanWriter' | 10000 | 6.066 us | 0.1118 us | 992 B | | 'High Perf Toolkit ArrayPoolWriter' | 10000 | 6.643 us | 0.1354 us | 12872 B | | 'MS RecyclableMemoryStream' | 10000 | 11.748 us | 0.3450 us | 872 B | | 'DotNext SparseBufferWriter' | 10000 | 12.834 us | 0.4699 us | 1336 B | | 'Proposed SpanWriter' | 100000 | 34.320 us | 0.4676 us | 992 B | | 'High Perf Toolkit ArrayPoolWriter' | 100000 | 38.629 us | 0.6964 us | 119744 B | | 'MS RecyclableMemoryStream' | 100000 | 59.608 us | 1.1849 us | 872 B | | 'DotNext SparseBufferWriter' | 100000 | 56.021 us | 0.9304 us | 3272 B | | 'Proposed SpanWriter' | 1000000 | 146.279 us | 2.9135 us | 992 B | | 'High Perf Toolkit ArrayPoolWriter' | 1000000 | 180.226 us | 1.3876 us | 1188488 B | | 'MS RecyclableMemoryStream' | 1000000 | 520.908 us | 2.8181 us | 1504 B | | 'DotNext SparseBufferWriter' | 1000000 | 553.517 us | 1.3129 us | 22632 B | ### Alternative Designs The growth increment could definitely be optimized based upon avg size written via linear growth, exponentiation or logistic regression/additive equation. ### Risks Taub will sh!1 on the idea. ### Reference implementation... https://github.com/adam-dot-cohen/ResizableSpanWriter
Author: adam-dot-cohen
Assignees: -
Labels: `api-suggestion`, `area-System.Buffers`, `untriaged`
Milestone: -
SpicyBits commented 1 year ago

Would be nice to have a ref struct flavor as well. Or, at least a few convenience methods on Span and Memory (e.g. Write, TryWrite, etc...).

sakno commented 1 year ago

I see DotNext presented in Benchmark) SparseBufferWriter doesn't implement contiguous buffer so its comparison with ArrayBufferWriter a bit incorrect. @adam-dot-cohen , PoolingBufferWriter is better alternative that exposes contiguous memory block. Also, it already implements IMemoryOwner<T>.

@SpicyBits , ref counterpart is also presented as BufferWriterSlim from DotNext.

adam-dot-cohen commented 1 year ago

@sakno - agreed on SparseBufferWriter it's not apples to apples. I switched the benchmarks to use PoolingBufferWriter. See benchmarks below...

Big fan of the DotNext library. This type is just a toy, not buffer growth optimization, etc... If you think it makes any sense in DotNext, feel free to copy it from the repo, or I'd be happy to pay the value I've received from DotNext backward and create a PR for you. Keep up the awesome work on the library!

Code can be found here...

Code can be found here... https://github.com/adam-dot-cohen/ResizableSpanWriter

BenchmarkDotNet=v0.13.4, OS=Windows 11 (10.0.22621.2283)
Intel Core i9-10980XE CPU 3.00GHz, 1 CPU, 36 logical and 18 physical cores
.NET SDK=8.0.100-preview.6.23330.14
  [Host]     : .NET 7.0.11 (7.0.1123.42427), X64 RyuJIT AVX2
  Job-YJBPHY : .NET 7.0.11 (7.0.1123.42427), X64 RyuJIT AVX2

Jit=RyuJit  Runtime=.NET 7.0  Arguments=/p:Optimize=true
InvocationCount=1  LaunchCount=1  RunStrategy=Throughput
UnrollFactor=1

|                              Method | TotalCount |       Mean |     Error | Ratio | Allocated | Alloc Ratio |
|------------------------------------ |----------- |-----------:|----------:|------:|----------:|------------:|
|       'Proposed MemoryBufferWriter' |        100 |   1.815 us | 0.0856 us |  1.00 |     992 B |        1.00 |
| 'High Perf Toolkit ArrayPoolWriter' |        100 |   1.841 us | 0.0808 us |  1.03 |     976 B |        0.98 |
|        'DotNext PooledBufferWriter' |        100 |   6.345 us | 0.3128 us |  3.56 |    1328 B |        1.34 |
|         'MS RecyclableMemoryStream' |        100 |   6.086 us | 0.2201 us |  3.43 |     872 B |        0.88 |
|                                     |            |            |           |       |           |             |
|       'Proposed MemoryBufferWriter' |       1000 |   2.192 us | 0.0912 us |  1.00 |     992 B |        1.00 |
| 'High Perf Toolkit ArrayPoolWriter' |       1000 |   2.248 us | 0.0632 us |  1.03 |     976 B |        0.98 |
|        'DotNext PooledBufferWriter' |       1000 |   6.730 us | 0.3361 us |  3.12 |    1328 B |        1.34 |
|         'MS RecyclableMemoryStream' |       1000 |   6.458 us | 0.2275 us |  2.99 |     872 B |        0.88 |
|                                     |            |            |           |       |           |             |
|       'Proposed MemoryBufferWriter' |      10000 |   2.437 us | 0.0906 us |  1.00 |     992 B |        1.00 |
| 'High Perf Toolkit ArrayPoolWriter' |      10000 |   3.628 us | 0.1447 us |  1.51 |     976 B |        0.98 |
|        'DotNext PooledBufferWriter' |      10000 |   8.089 us | 0.3166 us |  3.37 |    1328 B |        1.34 |
|         'MS RecyclableMemoryStream' |      10000 |   9.856 us | 0.2774 us |  4.09 |     872 B |        0.88 |
|                                     |            |            |           |       |           |             |
|       'Proposed MemoryBufferWriter' |     100000 |   7.320 us | 0.1485 us |  1.00 |     992 B |        1.00 |
| 'High Perf Toolkit ArrayPoolWriter' |     100000 |  13.107 us | 0.2647 us |  1.79 |     976 B |        0.98 |
|        'DotNext PooledBufferWriter' |     100000 |  19.373 us | 0.3802 us |  2.63 |    1328 B |        1.34 |
|         'MS RecyclableMemoryStream' |     100000 |  43.500 us | 0.8456 us |  5.92 |     872 B |        0.88 |
|                                     |            |            |           |       |           |             |
|       'Proposed MemoryBufferWriter' |    1000000 |  54.994 us | 1.0460 us |  1.00 |     992 B |        1.00 |
| 'High Perf Toolkit ArrayPoolWriter' |    1000000 | 105.674 us | 1.7592 us |  1.93 |     976 B |        0.98 |
|        'DotNext PooledBufferWriter' |    1000000 | 140.283 us | 0.3657 us |  2.55 |    1328 B |        1.34 |
|         'MS RecyclableMemoryStream' |    1000000 | 380.883 us | 1.8720 us |  6.92 |     872 B |        0.88 |
sakno commented 1 year ago

@adam-dot-cohen , it's better to continue conversation to dotnext repo. However, you can already achieve what you want with PoolingBufferWriter class and MemoryAllocator.GetArrayAllocator<T>() method to use regular arrays instead of pooling. Also, you can provide your own MemoryAllocator<T> delegate to override allocation strategy.

adam-dot-cohen commented 1 year ago

Closing this out and moving converation to DotNext