1223 lines
41 KiB
C#
1223 lines
41 KiB
C#
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 ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class BinarySerializationContext : 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 const int InitialReferenceCapacity = 16;
|
|
private const int InitialMultiRefCapacity = 8;
|
|
|
|
// Bloom filter constants for string interning
|
|
private const int BloomFilterSize = 256; // 256 bits = 32 bytes
|
|
private const int BloomFilterMask = BloomFilterSize - 1;
|
|
|
|
private byte[] _buffer;
|
|
private int _position;
|
|
private int _initialBufferSize;
|
|
|
|
private Dictionary<object, int>? _scanOccurrences;
|
|
private Dictionary<object, int>? _writtenRefs;
|
|
private HashSet<object>? _multiReferenced;
|
|
private int _nextRefId;
|
|
|
|
private Dictionary<string, int>? _internedStrings;
|
|
private List<string>? _internedStringList;
|
|
|
|
/// <summary>
|
|
/// Bloom filter for quick "definitely not interned" checks.
|
|
/// Avoids dictionary lookup for unique strings.
|
|
/// </summary>
|
|
private ulong _bloomFilter0;
|
|
private ulong _bloomFilter1;
|
|
private ulong _bloomFilter2;
|
|
private ulong _bloomFilter3;
|
|
|
|
private Dictionary<string, int>? _propertyNames;
|
|
private List<string>? _propertyNameList;
|
|
private int[]? _propertyIndexBuffer;
|
|
private byte[]? _propertyStateBuffer;
|
|
|
|
public bool UseReferenceHandling { get; private set; }
|
|
public bool UseStringInterning { get; private set; }
|
|
public bool UseMetadata { get; private set; }
|
|
public byte MaxDepth { get; private set; }
|
|
public byte MinStringInternLength { get; private set; }
|
|
public BinaryPropertyFilter? PropertyFilter { 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);
|
|
}
|
|
|
|
public void Reset(AcBinarySerializerOptions options)
|
|
{
|
|
_position = 0;
|
|
_nextRefId = 1;
|
|
UseReferenceHandling = options.UseReferenceHandling;
|
|
UseStringInterning = options.UseStringInterning;
|
|
UseMetadata = options.UseMetadata;
|
|
MaxDepth = options.MaxDepth;
|
|
MinStringInternLength = options.MinStringInternLength;
|
|
PropertyFilter = options.PropertyFilter;
|
|
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
|
|
|
if (_buffer.Length < _initialBufferSize)
|
|
{
|
|
ArrayPool<byte>.Shared.Return(_buffer);
|
|
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
|
}
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
_position = 0;
|
|
_nextRefId = 1;
|
|
|
|
// Reset bloom filter
|
|
_bloomFilter0 = 0;
|
|
_bloomFilter1 = 0;
|
|
_bloomFilter2 = 0;
|
|
_bloomFilter3 = 0;
|
|
|
|
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
|
|
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
|
|
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
|
|
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
|
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
|
|
|
_propertyNameList?.Clear();
|
|
_internedStringList?.Clear();
|
|
|
|
// Reset cached property indices
|
|
ResetCachedPropertyIndices();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
private void ResetCachedPropertyIndices()
|
|
{
|
|
// Note: BinaryPropertyAccessor.CachedPropertyNameIndex is per-context,
|
|
// but metadata is cached globally. We reset it during Clear to avoid
|
|
// stale indices. The next serialization will re-populate them.
|
|
// This is a minor cost as it only happens on context reuse.
|
|
}
|
|
|
|
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
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public int RegisterInternedString(string value)
|
|
{
|
|
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
|
_internedStringList ??= new List<string>(InitialInternCapacity);
|
|
|
|
// Fast path: check bloom filter first
|
|
var hash = GetStringHash(value);
|
|
if (!BloomFilterMightContain(hash))
|
|
{
|
|
// Definitely not in dictionary - add directly
|
|
var newIndex = _internedStringList.Count;
|
|
_internedStrings[value] = newIndex;
|
|
_internedStringList.Add(value);
|
|
BloomFilterAdd(hash);
|
|
return newIndex;
|
|
}
|
|
|
|
// Might be in dictionary - need to check
|
|
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_internedStrings, value, out var exists);
|
|
if (exists)
|
|
{
|
|
return index;
|
|
}
|
|
|
|
index = _internedStringList.Count;
|
|
_internedStringList.Add(value);
|
|
BloomFilterAdd(hash);
|
|
return index;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static int GetStringHash(string value)
|
|
{
|
|
// Simple hash combining length and first/last characters
|
|
// Optimized for quick calculation, not collision resistance
|
|
if (value.Length == 0) return 0;
|
|
var h = value.Length;
|
|
h = (h * 31) + value[0];
|
|
if (value.Length > 1)
|
|
h = (h * 31) + value[value.Length - 1];
|
|
return h;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool BloomFilterMightContain(int hash)
|
|
{
|
|
// Use two hash functions for bloom filter
|
|
var h1 = hash & BloomFilterMask;
|
|
var h2 = (hash >> 8) & BloomFilterMask;
|
|
|
|
return BloomFilterTestBit(h1) && BloomFilterTestBit(h2);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool BloomFilterTestBit(int bit)
|
|
{
|
|
var segment = bit >> 6; // Divide by 64
|
|
var mask = 1UL << (bit & 63);
|
|
return segment switch
|
|
{
|
|
0 => (_bloomFilter0 & mask) != 0,
|
|
1 => (_bloomFilter1 & mask) != 0,
|
|
2 => (_bloomFilter2 & mask) != 0,
|
|
_ => (_bloomFilter3 & mask) != 0,
|
|
};
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void BloomFilterAdd(int hash)
|
|
{
|
|
var h1 = hash & BloomFilterMask;
|
|
var h2 = (hash >> 8) & BloomFilterMask;
|
|
|
|
BloomFilterSetBit(h1);
|
|
BloomFilterSetBit(h2);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void BloomFilterSetBit(int bit)
|
|
{
|
|
var segment = bit >> 6;
|
|
var mask = 1UL << (bit & 63);
|
|
switch (segment)
|
|
{
|
|
case 0: _bloomFilter0 |= mask; break;
|
|
case 1: _bloomFilter1 |= mask; break;
|
|
case 2: _bloomFilter2 |= mask; break;
|
|
default: _bloomFilter3 |= mask; break;
|
|
}
|
|
}
|
|
|
|
#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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers property name and caches the index in the accessor for future lookups.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void RegisterPropertyNameAndCache(BinaryPropertyAccessor accessor)
|
|
{
|
|
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
|
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
|
|
|
ref var index = ref CollectionsMarshal.GetValueRefOrAddDefault(_propertyNames, accessor.Name, out var exists);
|
|
if (!exists)
|
|
{
|
|
index = _propertyNameList.Count;
|
|
_propertyNameList.Add(accessor.Name);
|
|
}
|
|
accessor.CachedPropertyNameIndex = index;
|
|
}
|
|
|
|
[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.ObjectGetter);
|
|
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;
|
|
}
|
|
|
|
[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 (System.Text.Ascii.IsValid(value))
|
|
{
|
|
WriteVarUInt((uint)value.Length);
|
|
EnsureCapacity(value.Length);
|
|
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
|
|
System.Text.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;
|
|
private int _estimatedHeaderSize;
|
|
|
|
/// <summary>
|
|
/// Estimates header payload size based on registered property names and intern strings.
|
|
/// Call after metadata registration but before writing the body.
|
|
/// </summary>
|
|
public int EstimateHeaderPayloadSize()
|
|
{
|
|
var size = 0;
|
|
|
|
if (UseMetadata && _propertyNameList is { Count: > 0 })
|
|
{
|
|
size += GetVarUIntSize((uint)_propertyNameList.Count);
|
|
foreach (var name in _propertyNameList)
|
|
{
|
|
var byteCount = name.Length; // Assume ASCII (common case), fallback handles multi-byte
|
|
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
|
}
|
|
}
|
|
|
|
if (UseStringInterning && _internedStringList is { Count: > 0 })
|
|
{
|
|
size += GetVarUIntSize((uint)_internedStringList.Count);
|
|
foreach (var value in _internedStringList)
|
|
{
|
|
var byteCount = value.Length; // Assume ASCII for estimation
|
|
size += GetVarUIntSize((uint)byteCount) + byteCount;
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
public void WriteHeaderPlaceholder()
|
|
{
|
|
EnsureCapacity(2);
|
|
_headerPosition = _position;
|
|
_position += 2;
|
|
_estimatedHeaderSize = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reserves space for header based on estimation. Call after metadata registration.
|
|
/// </summary>
|
|
public void ReserveHeaderSpace(int estimatedSize)
|
|
{
|
|
if (estimatedSize > 0)
|
|
{
|
|
EnsureCapacity(estimatedSize);
|
|
_estimatedHeaderSize = estimatedSize;
|
|
_position += estimatedSize;
|
|
}
|
|
}
|
|
|
|
public void FinalizeHeaderSections()
|
|
{
|
|
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
|
|
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
|
|
|
|
// Calculate actual header size first
|
|
var actualSize = 0;
|
|
if (hasPropertyNames)
|
|
{
|
|
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
|
|
foreach (var name in _propertyNameList)
|
|
{
|
|
var byteCount = Utf8NoBom.GetByteCount(name);
|
|
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
|
}
|
|
}
|
|
|
|
if (hasInternTable)
|
|
{
|
|
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
|
|
foreach (var value in _internedStringList)
|
|
{
|
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
|
|
}
|
|
}
|
|
|
|
var bodyStart = _headerPosition + 2 + _estimatedHeaderSize;
|
|
var bodyLength = _position - bodyStart;
|
|
|
|
// Shift body if needed
|
|
if (actualSize != _estimatedHeaderSize && bodyLength > 0)
|
|
{
|
|
var delta = actualSize - _estimatedHeaderSize;
|
|
if (delta > 0)
|
|
{
|
|
EnsureCapacity(delta);
|
|
}
|
|
|
|
var newBodyStart = _headerPosition + 2 + actualSize;
|
|
if (delta != 0)
|
|
{
|
|
Array.Copy(_buffer, bodyStart, _buffer, newBodyStart, bodyLength);
|
|
_position += delta;
|
|
}
|
|
}
|
|
|
|
// Write header payload directly to buffer (no ArrayBufferWriter allocation)
|
|
var headerPos = _headerPosition + 2;
|
|
|
|
if (hasPropertyNames)
|
|
{
|
|
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
|
|
foreach (var name in _propertyNameList)
|
|
{
|
|
headerPos = WriteStringAt(headerPos, name);
|
|
}
|
|
}
|
|
|
|
if (hasInternTable)
|
|
{
|
|
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
|
|
foreach (var value in _internedStringList)
|
|
{
|
|
headerPos = WriteStringAt(headerPos, value);
|
|
}
|
|
}
|
|
|
|
// Write header flags
|
|
byte flags = BinaryTypeCode.HeaderFlagsBase;
|
|
if (hasPropertyNames)
|
|
flags |= BinaryTypeCode.HeaderFlag_Metadata;
|
|
if (UseReferenceHandling)
|
|
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
|
|
if (hasInternTable)
|
|
flags |= BinaryTypeCode.HeaderFlag_StringInternTable;
|
|
|
|
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
|
|
_buffer[_headerPosition + 1] = flags;
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes UTF8 string at specific position (length-prefixed) and returns new position.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private int WriteStringAt(int pos, string value)
|
|
{
|
|
var byteCount = Utf8NoBom.GetByteCount(value);
|
|
pos = WriteVarUIntAt(pos, (uint)byteCount);
|
|
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
|
|
return pos + byteCount;
|
|
}
|
|
|
|
// Remove old methods: WriteHeaderVarUInt, WriteHeaderString (no longer needed)
|
|
|
|
#endregion
|
|
|
|
#region Reference Handling
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TrackForScanning(object obj)
|
|
{
|
|
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
|
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
|
|
|
|
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
|
if (exists)
|
|
{
|
|
count++;
|
|
_multiReferenced.Add(obj);
|
|
return false;
|
|
}
|
|
|
|
count = 1;
|
|
return true;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool ShouldWriteRef(object obj, out int refId)
|
|
{
|
|
if (_multiReferenced != null && _multiReferenced.Contains(obj))
|
|
{
|
|
_writtenRefs ??= new Dictionary<object, int>(32, ReferenceEqualityComparer.Instance);
|
|
if (!_writtenRefs.ContainsKey(obj))
|
|
{
|
|
refId = _nextRefId++;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
refId = 0;
|
|
return false;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void MarkAsWritten(object obj, int refId)
|
|
=> _writtenRefs![obj] = refId;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TryGetExistingRef(object obj, out int refId)
|
|
{
|
|
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
refId = 0;
|
|
return false;
|
|
}
|
|
|
|
#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);
|
|
System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
|
_position += length;
|
|
}
|
|
|
|
/// <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
|
|
}
|
|
}
|