135 lines
4.9 KiB
C#
135 lines
4.9 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace AyCode.Core.Serializers.Binaries;
|
|
|
|
/// <summary>
|
|
/// High-performance binary output backed by a byte[] from ArrayPool.
|
|
/// Only handles buffer lifecycle (Initialize, Grow, result extraction).
|
|
/// All write methods live in BinarySerializationContext for zero virtual dispatch.
|
|
///
|
|
/// This is the fastest output path — use when the result is needed as byte[]/Span.
|
|
/// </summary>
|
|
public struct ArrayBinaryOutput : IBinaryOutputBase, IDisposable
|
|
{
|
|
private const int DefaultMinBufferSize = 256;
|
|
private const int MaxKeepBufferSize = 32 * 1024; // 32KB — below this, keep for reuse
|
|
|
|
private readonly int _initialCapacity;
|
|
private byte[] _rentedBuffer;
|
|
|
|
public ArrayBinaryOutput(int initialCapacity = 65535)
|
|
{
|
|
_initialCapacity = Math.Max(initialCapacity, DefaultMinBufferSize);
|
|
_rentedBuffer = ArrayPool<byte>.Shared.Rent(_initialCapacity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides the initial buffer. Position starts at 0, bufferEnd = buffer.Length.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Initialize(out byte[] buffer, out int position, out int bufferEnd)
|
|
{
|
|
buffer = _rentedBuffer;
|
|
position = 0;
|
|
bufferEnd = _rentedBuffer.Length;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Grows the buffer: rents a bigger one from ArrayPool, copies old data, returns old to pool.
|
|
/// Position is unchanged (data was copied to the same offset in the new buffer).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
public void Grow(ref byte[] buffer, ref int position, ref int bufferEnd, int needed)
|
|
{
|
|
var required = position + needed;
|
|
var newSize = Math.Max(buffer.Length * 2, required);
|
|
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
|
buffer.AsSpan(0, position).CopyTo(newBuffer);
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
buffer = newBuffer;
|
|
bufferEnd = newBuffer.Length;
|
|
_rentedBuffer = newBuffer;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For ArrayBinaryOutput, position IS the total (single contiguous buffer).
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public int GetTotalPosition(int currentPosition) => currentPosition;
|
|
|
|
#region Result Extraction — receive buffer/position from context
|
|
|
|
//TODO: miért nem static a AsSpan?
|
|
/// <summary>Returns the written data as a ReadOnlySpan without allocation.</summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public ReadOnlySpan<byte> AsSpan(byte[] buffer, int position) => buffer.AsSpan(0, position);
|
|
|
|
//TODO: miért nem static a ToArray? Miért nem valami static common osztályban van?
|
|
/// <summary>Copies the written data to a new exactly-sized array.</summary>
|
|
public byte[] ToArray(byte[] buffer, int position)
|
|
{
|
|
var result = GC.AllocateUninitializedArray<byte>(position);
|
|
buffer.AsSpan(0, position).CopyTo(result);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Copies the written data to an IBufferWriter (single memcpy).</summary>
|
|
public void WriteTo(IBufferWriter<byte> writer, byte[] buffer, int position)
|
|
{
|
|
var span = writer.GetSpan(position);
|
|
buffer.AsSpan(0, position).CopyTo(span);
|
|
writer.Advance(position);
|
|
}
|
|
|
|
//TODO: miért nem static a DetachResult?
|
|
/// <summary>
|
|
/// Detaches the internal buffer as a BinarySerializationResult and allocates a fresh buffer.
|
|
/// The caller owns the returned result and must dispose it to return the buffer to the pool.
|
|
/// </summary>
|
|
public AcBinarySerializer.BinarySerializationResult DetachResult(byte[] buffer, int position)
|
|
{
|
|
var resultBuffer = buffer;
|
|
var resultLength = position;
|
|
|
|
//_rentedBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, _initialCapacity));
|
|
|
|
return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets for reuse when context is returned to pool.
|
|
/// Small buffers (≤ MaxKeepBufferSize): keep as-is (faster than pool round-trip).
|
|
/// Large buffers: return to pool, rent a halved one to avoid memory bloat.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
if (_rentedBuffer == null) return;
|
|
|
|
// Small buffer: keep as-is
|
|
if (_rentedBuffer.Length <= MaxKeepBufferSize) return;
|
|
|
|
// Large buffer: return to pool, rent half size
|
|
var nextCapacity = Math.Max(_rentedBuffer.Length / 2, _initialCapacity);
|
|
ArrayPool<byte>.Shared.Return(_rentedBuffer);
|
|
|
|
_rentedBuffer = nextCapacity == _initialCapacity ? null : ArrayPool<byte>.Shared.Rent(nextCapacity);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IDisposable
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_rentedBuffer != null)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_rentedBuffer);
|
|
_rentedBuffer = null!;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|