AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySe...

1255 lines
44 KiB
C#
Raw Blame History

using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Text;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
private static class BinarySerializationContextPool
{
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
private const int MaxPoolSize = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
{
if (Pool.TryDequeue(out var context))
{
context.Reset(options);
return context;
}
return new BinarySerializationContext(options);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(BinarySerializationContext context)
{
if (Pool.Count < MaxPoolSize)
{
context.Clear();
Pool.Enqueue(context);
}
else
{
context.Dispose();
}
}
}
public static int GrowBufferCount =>
#if DEBUG
BinarySerializationContext.GrowBufferCount;
#else
-1;
#endif
public static long GrowBufferTotalBytes =>
#if DEBUG
BinarySerializationContext.GrowBufferTotalBytes;
#else
-1;
#endif
/// <summary>
/// Binary serialization context. Public for generated serializers.
/// </summary>
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
{
private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
private const int InitialInternCapacity = 32;
private const int InitialPropertyNameCapacity = 32;
private byte[] _buffer;
private int _position;
private int _initialBufferSize;
#if DEBUG
/// <summary>
/// Counts how many times GrowBuffer was called during serialization.
/// Used for benchmarking buffer allocation efficiency.
/// </summary>
public static int GrowBufferCount { get; set; }
/// <summary>
/// Total bytes allocated by GrowBuffer during serialization.
/// Used for benchmarking buffer allocation efficiency.
/// </summary>
public static long GrowBufferTotalBytes { get; set; }
#endif
// Use shared reference tracker from AcSerializerCommon
//private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
/// <summary>
/// String intern entry for tracking string occurrences.
/// StreamPosition-based approach for 100% reliable cache matching.
/// </summary>
private struct StringInternEntry
{
public int StreamPosition; // Position in stream where string was first written
public int CacheIndex; // Dense cache index (0, 1, 2, ...) - assigned at 2nd occurrence; -1 = first occurrence only
}
private Dictionary<string, StringInternEntry>? _stringInternMap;
private int _nextCacheIndex; // Next dense cache index to assign
private Dictionary<string, int>? _propertyNames;
private List<string>? _propertyNameList;
private int[]? _propertyIndexBuffer;
private byte[]? _propertyStateBuffer;
#if DEBUG
/// <summary>
/// DEBUG ONLY: Current property path being serialized (e.g., "Order.Status").
/// Used for string interning analysis.
/// </summary>
internal string? CurrentPropertyPath;
/// <summary>
/// DEBUG ONLY: Callback invoked when a string is registered for interning.
/// Parameters: (propertyPath, stringValue)
/// Use this to analyze which properties have repeated string values.
/// </summary>
internal Action<string?, string>? OnStringInterned;
#endif
// These properties delegate to Options for convenience
public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None;
public bool UseMetadata => Options.UseMetadata;
public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength;
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
/// <summary>
/// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop.
/// </summary>
public bool HasPropertyFilter { get; private set; }
public int Position => _position;
public BinarySerializationContext(AcBinarySerializerOptions options)
{
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
Reset(options);
}
/// <summary>
/// Factory for creating BinarySerializeTypeMetadata instances.
/// </summary>
protected override Func<Type, BinarySerializeTypeMetadata> MetadataFactory
=> static t => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute);
public override void Reset(AcBinarySerializerOptions options)
{
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
base.Reset(options);
_position = 0;
_initialBufferSize = Math.Max(Options.InitialBufferCapacity, MinBufferSize);
HasPropertyFilter = Options.PropertyFilter != null;
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
// Kumulatívan gyűjtjük a benchmark során.
if (_buffer.Length < _initialBufferSize)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
}
}
public void Clear()
{
_position = 0;
//_refTracker.Reset();
ClearAndTrimIfNeeded(_stringInternMap, InitialInternCapacity * 4);
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
_propertyNameList?.Clear();
_nextCacheIndex = 0;
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
// NOTE: GrowBufferCount <20>s GrowBufferTotalBytes nem null<6C>z<EFBFBD>dik itt,
// hogy a m<>r<EFBFBD>sek v<>g<EFBFBD>n ki tudj<64>k <20>rni az <20>rt<72>keket.
// Csak a Reset() met<65>dusban null<6C>z<EFBFBD>dnak minden <20>j fut<75>s elej<65>n.
}
public void Dispose()
{
if (_buffer != null)
{
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null!;
}
if (_propertyIndexBuffer != null)
{
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
_propertyIndexBuffer = null;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
_propertyStateBuffer = null;
}
}
#region String Interning
/// <summary>
/// Tries to intern a string. Returns true if string was seen before (write index).
/// Returns false if first occurrence (write inline).
/// Uses stream position for 100% reliable deserializer cache matching.
/// </summary>
/// <param name="value">The string value to intern</param>
/// <param name="streamPosition">Current stream position (before writing the string)</param>
/// <param name="cacheIndex">Output: cache index for 2+ occurrence, -1 for 1st occurrence</param>
/// <returns>True if 2+ occurrence (write cacheIndex), false if 1st occurrence (write inline)</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetInternedString(string value, int streamPosition, out int cacheIndex)
{
_stringInternMap ??= new Dictionary<string, StringInternEntry>(InitialInternCapacity, StringComparer.Ordinal);
ref var entry = ref CollectionsMarshal.GetValueRefOrNullRef(_stringInternMap, value);
if (!Unsafe.IsNullRef(ref entry))
{
// 2+ occurrence: assign CacheIndex if first repeat
if (entry.CacheIndex < 0)
{
entry.CacheIndex = _nextCacheIndex++;
}
cacheIndex = entry.CacheIndex;
return true;
}
// 1st occurrence: store stream position
_stringInternMap[value] = new StringInternEntry
{
StreamPosition = streamPosition,
CacheIndex = -1 // Not assigned until 2nd occurrence
};
cacheIndex = -1;
return false;
}
/// <summary>
/// Returns true if there are any interned strings that occurred more than once.
/// </summary>
public bool HasInternedStrings => _stringInternMap is { Count: > 0 };
/// <summary>
/// Gets the count of strings that occurred more than once (for footer).
/// </summary>
public int GetDupCount() => _nextCacheIndex;
/// <summary>
/// Writes the footer with (position, cacheIndex) pairs sorted by position.
/// VarUInt format for compact size, deserializer reads into flat int[].
/// </summary>
public void WriteInternedStringFooter()
{
if (_stringInternMap == null || _nextCacheIndex == 0) return;
// Collect entries with CacheIndex >= 0 (occurred more than once)
// We need to sort by StreamPosition for deserializer sequential access
Span<(int Position, int CacheIndex)> entries = _nextCacheIndex <= 64
? stackalloc (int, int)[_nextCacheIndex]
: new (int, int)[_nextCacheIndex];
var idx = 0;
foreach (var entry in _stringInternMap.Values)
{
if (entry.CacheIndex >= 0)
{
entries[idx++] = (entry.StreamPosition, entry.CacheIndex);
}
}
// Sort by StreamPosition (ascending) for deserializer sequential check
entries.Sort((a, b) => a.Position.CompareTo(b.Position));
// Write pairs as VarUInt for compact size
for (var i = 0; i < _nextCacheIndex; i++)
{
WriteVarUInt((uint)entries[i].Position);
WriteVarUInt((uint)entries[i].CacheIndex);
}
}
#endregion
#region Property Name Table
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterPropertyName(string name)
{
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
if (!_propertyNames.ContainsKey(name))
{
var index = _propertyNameList.Count;
_propertyNames[name] = index;
_propertyNameList.Add(name);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPropertyNameIndex(string name)
=> _propertyNames != null && _propertyNames.TryGetValue(name, out var index) ? index : -1;
#endregion
#region Property State Buffer
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] RentPropertyStateBuffer(int size)
{
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
{
return _propertyStateBuffer;
}
if (_propertyStateBuffer != null)
{
ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
}
_propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
return _propertyStateBuffer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyStateBuffer(byte[] buffer)
{
// Buffer stays cached for reuse.
}
#endregion
#region Output
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result);
return result;
}
public void WriteTo(IBufferWriter<byte> writer)
{
var span = writer.GetSpan(_position);
_buffer.AsSpan(0, _position).CopyTo(span);
writer.Advance(_position);
}
public BinarySerializationResult DetachResult()
{
var resultBuffer = _buffer;
var resultLength = _position;
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
_position = 0;
return new BinarySerializationResult(resultBuffer, resultLength, pooled: true);
}
#endregion
#region Property Filtering
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
{
if (PropertyFilter == null)
{
return true;
}
var context = new BinaryPropertyFilterContext(
instance,
property.DeclaringType,
property.Name,
property.PropertyType,
property.DynamicGetter);
return PropertyFilter(context);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool ShouldIncludePropertyInMetadata(BinaryPropertyAccessor property)
{
if (PropertyFilter == null)
{
return true;
}
var context = new BinaryPropertyFilterContext(
null,
property.DeclaringType,
property.Name,
property.PropertyType,
null);
return PropertyFilter(context);
}
#endregion
#region Buffer Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int additionalBytes)
{
var required = _position + additionalBytes;
if (required <= _buffer.Length)
{
return;
}
GrowBuffer(required);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowBuffer(int required)
{
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;
#if DEBUG
GrowBufferCount++;
GrowBufferTotalBytes += newSize;
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteByte(byte value)
{
if (_position >= _buffer.Length)
{
GrowBuffer(_position + 1);
}
_buffer[_position++] = value;
}
/// <summary>
/// Write type code byte followed by a raw value. Batches EnsureCapacity call.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
{
var size = 1 + Unsafe.SizeOf<T>();
EnsureCapacity(size);
_buffer[_position++] = typeCode;
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += Unsafe.SizeOf<T>();
}
/// <summary>
/// Write two bytes efficiently.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteTwoBytes(byte b1, byte b2)
{
EnsureCapacity(2);
_buffer[_position++] = b1;
_buffer[_position++] = b2;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> data)
{
EnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteRaw<T>(T value) where T : unmanaged
{
var size = Unsafe.SizeOf<T>();
EnsureCapacity(size);
Unsafe.WriteUnaligned(ref _buffer[_position], value);
_position += size;
}
#endregion
#region Specialized Writers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeBits(DateTime value)
{
EnsureCapacity(9);
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
_buffer[_position + 8] = (byte)value.Kind;
_position += 9;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDateTimeOffsetBits(DateTimeOffset value)
{
EnsureCapacity(10);
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
_position += 10;
}
/// <summary>
/// Patches a previously written VarUInt at the specified position.
/// Works correctly only if the new value requires the same or fewer bytes.
/// For property counts < 128, this is always 1 byte.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void PatchVarUInt(int position, uint value)
{
// Fast path: single byte (covers 0-127, which is most property counts)
if (value < 0x80)
{
_buffer[position] = (byte)value;
return;
}
// Multi-byte case - need to shift buffer if new encoding is longer
// For simplicity, we'll rewrite from the position
// This is rare for property counts
PatchVarUIntSlow(position, value);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void PatchVarUIntSlow(int position, uint value)
{
// Calculate current size at position (read until no continuation bit)
var currentSize = 0;
var pos = position;
while (pos < _position && (_buffer[pos] & 0x80) != 0)
{
currentSize++;
pos++;
}
currentSize++; // Include final byte without continuation bit
// Calculate new size needed
var newSize = GetVarUIntSize(value);
if (newSize == currentSize)
{
// Same size - just overwrite
var tempPos = position;
while (value >= 0x80)
{
_buffer[tempPos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[tempPos] = (byte)value;
}
else if (newSize < currentSize)
{
// New is smaller - shift data left
var delta = currentSize - newSize;
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
_position -= delta;
var tempPos = position;
while (value >= 0x80)
{
_buffer[tempPos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[tempPos] = (byte)value;
}
else
{
// New is larger - shift data right
var delta = newSize - currentSize;
EnsureCapacity(delta);
Array.Copy(_buffer, position + currentSize, _buffer, position + newSize, _position - position - currentSize);
_position += delta;
var tempPos = position;
while (value >= 0x80)
{
_buffer[tempPos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[tempPos] = (byte)value;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetVarUIntSize(uint value)
{
if (value < 0x80) return 1;
if (value < 0x4000) return 2;
if (value < 0x200000) return 3;
if (value < 0x10000000) return 4;
return 5;
}
public void WriteVarInt(int value)
{
var encoded = (uint)((value << 1) ^ (value >> 31));
// Fast path for small positive values (0-63 when ZigZag encoded)
if (encoded < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)encoded;
return;
}
EnsureCapacity(5);
WriteVarUIntInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
// Fast path for small values (0-127)
if (value < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(5);
WriteVarUIntInternal(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteVarUIntInternal(uint value)
{
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarLong(long value)
{
var encoded = (ulong)((value << 1) ^ (value >> 63));
// Fast path for small values
if (encoded < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)encoded;
return;
}
EnsureCapacity(10);
WriteVarULongInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
// Fast path for small values (0-127)
if (value < 0x80)
{
EnsureCapacity(1);
_buffer[_position++] = (byte)value;
return;
}
EnsureCapacity(10);
WriteVarULongInternal(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteVarULongInternal(ulong value)
{
while (value >= 0x80)
{
_buffer[_position++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[_position++] = (byte)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringUtf8(string value)
{
// Fast path for ASCII-only strings using SIMD-optimized check
if (Ascii.IsValid(value))
{
WriteVarUInt((uint)value.Length);
EnsureCapacity(value.Length);
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
_position += value.Length;
return;
}
// Standard path for multi-byte UTF8
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary>
/// Checks if string contains only ASCII characters (0-127).
/// Optimized loop with early exit.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAscii(string value)
{
var span = value.AsSpan();
for (var i = 0; i < span.Length; i++)
{
if (span[i] > 127)
return false;
}
return true;
}
/// <summary>
/// Writes ASCII string directly to byte buffer (char to byte, no encoding needed).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteAsciiDirect(ReadOnlySpan<char> source, Span<byte> destination)
{
for (var i = 0; i < source.Length; i++)
{
destination[i] = (byte)source[i];
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
{
WriteByte(BinaryTypeCode.String);
WriteVarUInt((uint)utf8Name.Length);
WriteBytes(utf8Name);
}
public void WriteInt32ArrayOptimized(int[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(value);
}
}
}
public void WriteLongArrayOptimized(long[] array)
{
for (var i = 0; i < array.Length; i++)
{
var value = array[i];
if (value >= int.MinValue && value <= int.MaxValue)
{
var intValue = (int)value;
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
{
WriteByte(tiny);
}
else
{
WriteByte(BinaryTypeCode.Int32);
WriteVarInt(intValue);
}
}
else
{
WriteByte(BinaryTypeCode.Int64);
WriteVarLong(value);
}
}
}
public void WriteDoubleArrayBulk(double[] array)
{
EnsureCapacity(array.Length * 9);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float64;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 8;
}
}
public void WriteFloatArrayBulk(float[] array)
{
EnsureCapacity(array.Length * 5);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Float32;
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
_position += 4;
}
}
public void WriteGuidArrayBulk(Guid[] array)
{
EnsureCapacity(array.Length * 17);
for (var i = 0; i < array.Length; i++)
{
_buffer[_position++] = BinaryTypeCode.Guid;
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
}
#endregion
#region SIMD Bulk Copy
/// <summary>
/// Copy bytes using SIMD when available, otherwise fall back to standard copy.
/// Optimized for Blazor WASM where Vector operations are supported.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytesSimd(ReadOnlySpan<byte> source)
{
EnsureCapacity(source.Length);
var destination = _buffer.AsSpan(_position, source.Length);
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
{
CopyWithSimd(source, destination);
}
else
{
source.CopyTo(destination);
}
_position += source.Length;
}
/// <summary>
/// SIMD-optimized memory copy for large buffers.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CopyWithSimd(ReadOnlySpan<byte> source, Span<byte> destination)
{
var vectorSize = Vector<byte>.Count;
var i = 0;
var length = source.Length;
// Process full vectors
var vectorCount = length / vectorSize;
for (var v = 0; v < vectorCount; v++)
{
var vec = new Vector<byte>(source.Slice(i, vectorSize));
vec.CopyTo(destination.Slice(i, vectorSize));
i += vectorSize;
}
// Copy remaining bytes
if (i < length)
{
source.Slice(i).CopyTo(destination.Slice(i));
}
}
/// <summary>
/// Write double array using SIMD bulk copy (no per-element type codes).
/// For use when caller handles type codes separately.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDoubleBulkRaw(ReadOnlySpan<double> values)
{
var byteSpan = MemoryMarshal.AsBytes(values);
WriteBytesSimd(byteSpan);
}
/// <summary>
/// Write float array using SIMD bulk copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFloatBulkRaw(ReadOnlySpan<float> values)
{
var byteSpan = MemoryMarshal.AsBytes(values);
WriteBytesSimd(byteSpan);
}
/// <summary>
/// Write Guid array using SIMD bulk copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBulkRaw(ReadOnlySpan<Guid> values)
{
// Guid is 16 bytes, perfect for SIMD
var byteLength = values.Length * 16;
EnsureCapacity(byteLength);
for (var i = 0; i < values.Length; i++)
{
values[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
}
#endregion
#region Header and Metadata
private int _headerPosition;
// Footer-based string interning: no estimation or shifting needed
// Header: [version][flags][footerPosition (4 bytes, only if string interning)]
// Body: data with StringInterned indices
// Footer: interned strings table
/// <summary>
/// Estimates header payload size based on registered property names.
/// String interning now uses footer, so no estimation needed for strings.
/// </summary>
public int EstimateHeaderPayloadSize()
{
var size = 0;
// Only property names are in header now
if (UseMetadata && _propertyNameList is { Count: > 0 })
{
size += GetVarUIntSize((uint)_propertyNameList.Count);
for (var i = 0; i < _propertyNameList.Count; i++)
{
var name = _propertyNameList[i];
var byteCount = name.Length; // Assume ASCII (common case)
size += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
return size;
}
public void WriteHeaderPlaceholder()
{
// Header layout:
// [0] version (1 byte)
// [1] flags (1 byte)
// [2-5] footer position (4 bytes, only if UseStringInterning)
EnsureCapacity(UseStringInterning ? 6 : 2);
_headerPosition = _position;
_position += UseStringInterning ? 6 : 2;
}
/// <summary>
/// Reserves space for property name table in header.
/// </summary>
public void ReserveHeaderSpace(int estimatedSize)
{
if (estimatedSize <= 0) return;
EnsureCapacity(estimatedSize);
_position += estimatedSize;
}
public void FinalizeHeaderSections()
{
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
var dupCount = UseStringInterning ? GetDupCount() : 0;
var hasInternTable = dupCount > 0;
// Calculate property names header size (strings go to footer now)
var headerPayloadSize = 0;
if (hasPropertyNames)
{
headerPayloadSize += GetVarUIntSize((uint)_propertyNameList!.Count);
for (var i = 0; i < _propertyNameList.Count; i++)
{
var name = _propertyNameList[i];
var byteCount = Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
headerPayloadSize += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
// Write property names to header if needed
var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2);
if (hasPropertyNames)
{
var headerPos = headerPayloadStart;
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
for (var i = 0; i < _propertyNameList.Count; i++)
{
var name = _propertyNameList[i];
headerPos = WriteStringAtOptimized(headerPos, name);
}
}
// Footer: write indices of strings that occurred more than once
var footerPosition = 0;
if (hasInternTable)
{
footerPosition = _position;
WriteFooterStringIndices(dupCount);
}
// Write header
var flags = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
flags |= BinaryTypeCode.HeaderFlag_Metadata;
// Encode ReferenceHandlingMode using separate bits
if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
else if (ReferenceHandling == ReferenceHandlingMode.All)
flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
// Set footer position flag if string interning is enabled
if (UseStringInterning)
flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags;
// Always write footer position if string interning is enabled in options
// (even if there's no actual interned data - footer position will be 0)
if (UseStringInterning)
{
Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition);
}
}
/// <summary>
/// Writes the footer with total count (for verification) + dup count + indices.
/// Footer format: [totalStringCount][dupCount][dupIndex0][dupIndex1]...
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <summary>
/// Writes footer: [dupCount][(position, cacheIndex), ...]
/// Position-based format for 100% reliable deserializer matching.
/// </summary>
private void WriteFooterStringIndices(int dupCount)
{
// Dup count + (position, cacheIndex) pairs
WriteVarUInt((uint)dupCount);
WriteInternedStringFooter();
}
/// <summary>
/// Writes UTF8 string at specific position, optimized for ASCII strings.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int WriteStringAtOptimized(int pos, string value)
{
// Fast path for ASCII strings
if (Ascii.IsValid(value))
{
pos = WriteVarUIntAt(pos, (uint)value.Length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
return pos + value.Length;
}
// Standard path for multi-byte UTF8
var byteCount = Utf8NoBom.GetByteCount(value);
pos = WriteVarUIntAt(pos, (uint)byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
return pos + byteCount;
}
/// <summary>
/// Writes VarUInt at specific position and returns new position.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int WriteVarUIntAt(int pos, uint value)
{
while (value >= 0x80)
{
_buffer[pos++] = (byte)(value | 0x80);
value >>= 7;
}
_buffer[pos++] = (byte)value;
return pos;
}
#endregion
#region Reference Handling
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj);
/// <summary>
/// IId-aware tracking for the scan phase.
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals.
/// </summary>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
//{
// if (!ReferenceHandling)
// {
// existingRefId = 0;
// return true; // No tracking needed
// }
// return _refTracker.TrackForScanningWithIId(obj, metadata, out existingRefId);
//}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public bool ShouldWriteRef(object obj, out int refId) => _refTracker.ShouldWriteId(obj, out refId);
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public void MarkAsWritten(object obj, int refId) => _refTracker.MarkAsWritten(obj, refId);
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public bool TryGetExistingRef(object obj, out int refId) => _refTracker.TryGetExistingRef(obj, out refId);
#endregion
#region Helpers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity)
where TKey : notnull
{
if (dict == null)
{
return;
}
dict.Clear();
if (dict.EnsureCapacity(0) > maxCapacity)
{
dict.TrimExcess();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
{
if (set == null)
{
return;
}
set.Clear();
if (set.EnsureCapacity(0) > maxCapacity)
{
set.TrimExcess();
}
}
#endregion
#region FixStr Methods
/// <summary>
/// Write short ASCII string using FixStr encoding (type+length in single byte).
/// Only call when string is ASCII and length <= 31.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixStr(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
_position += length;
}
/// <summary>
/// Optimized FixStr write: tries SIMD ASCII conversion, falls back to UTF8.
/// Single-pass: uses Ascii.FromUtf16 which does validation + copy.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixStrDirect(string value)
{
var length = value.Length;
EnsureCapacity(1 + length);
// Ascii.FromUtf16: SIMD-optimized ASCII conversion
// Returns actual bytes written - if less than input length, there was a non-ASCII char
var destSpan = _buffer.AsSpan(_position + 1, length);
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
{
// Success - write FixStr header
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
_position += 1 + length;
}
else
{
// Non-ASCII or partial - use standard string encoding
_buffer[_position++] = BinaryTypeCode.String;
WriteStringUtf8Internal(value);
}
}
/// <summary>
/// Internal string write (after String type code already written).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteStringUtf8Internal(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
/// <summary>
/// Write short UTF8 bytes using FixStr encoding.
/// Only call when byteLength <= 31.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
{
var length = utf8Bytes.Length;
EnsureCapacity(1 + length);
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
_position += length;
}
#endregion
}
}