Enhance AcBinary: property filter, string interning, arrays

- Add property-level filtering via BinaryPropertyFilter delegate and context
- Improve string interning with new StringInternNew type code and promotion logic
- Optimize array and dictionary serialization for primitive types
- Expose strongly-typed property accessors for primitives and enums
- Add new benchmarks for serialization modes
- Refactor buffer pooling and cleanup code
- All new features are opt-in; maintains backward compatibility
This commit is contained in:
Loretta 2025-12-14 12:45:29 +01:00
parent 5601c0d3e2
commit 271f23d0f6
4 changed files with 1217 additions and 977 deletions

View File

@ -465,4 +465,80 @@ public class SizeComparisonBenchmark
[Benchmark(Description = "Placeholder")]
public int Placeholder() => 1; // Just to make BenchmarkDotNet happy
}
public enum BinaryBenchmarkMode
{
Default,
NoReferenceHandling,
FastMode
}
public abstract class AcBinaryOptionsBenchmarkBase
{
protected TestOrder TestOrder = null!;
protected AcBinarySerializerOptions BinaryOptions = null!;
protected MessagePackSerializerOptions MsgPackOptions = null!;
protected byte[] AcBinaryData = null!;
protected byte[] MsgPackData = null!;
[Params(BinaryBenchmarkMode.Default, BinaryBenchmarkMode.NoReferenceHandling, BinaryBenchmarkMode.FastMode)]
public BinaryBenchmarkMode Mode { get; set; }
[GlobalSetup]
public void GlobalSetup()
{
TestDataFactory.ResetIdCounter();
TestOrder = TestDataFactory.CreateBenchmarkOrder(
itemCount: 4,
palletsPerItem: 3,
measurementsPerPallet: 3,
pointsPerMeasurement: 6);
BinaryOptions = CreateBinaryOptions(Mode);
MsgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
AcBinaryData = AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
MsgPackData = MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
var ratio = MsgPackData.Length == 0 ? 0 : 100.0 * AcBinaryData.Length / MsgPackData.Length;
Console.WriteLine($"[BenchmarkSetup] Mode={Mode} | AcBinary={AcBinaryData.Length} bytes | MessagePack={MsgPackData.Length} bytes | Ratio={ratio:F1}%");
}
private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch
{
BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(),
BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling(),
BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions
{
UseMetadata = false,
UseStringInterning = false,
UseReferenceHandling = false
},
_ => new AcBinarySerializerOptions()
};
}
[ShortRunJob]
[MemoryDiagnoser]
[RankColumn]
public class AcBinaryOptionsSerializeBenchmark : AcBinaryOptionsBenchmarkBase
{
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
public byte[] Serialize_MessagePack() => MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
[Benchmark(Description = "AcBinary Serialize")]
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
}
[ShortRunJob]
[MemoryDiagnoser]
[RankColumn]
public class AcBinaryOptionsDeserializeBenchmark : AcBinaryOptionsBenchmarkBase
{
[Benchmark(Description = "MessagePack Deserialize", Baseline = true)]
public TestOrder? Deserialize_MessagePack() => MessagePackSerializer.Deserialize<TestOrder>(MsgPackData, MsgPackOptions);
[Benchmark(Description = "AcBinary Deserialize")]
public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder>(AcBinaryData);
}

View File

@ -67,9 +67,10 @@ public static class AcBinaryDeserializer
RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx));
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadPlainString(ref ctx));
RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
RegisterReader(BinaryTypeCode.StringInternNew, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndRegisterInternedString(ref ctx));
RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
@ -281,6 +282,30 @@ public static class AcBinaryDeserializer
context.Position, targetType);
}
/// <summary>
/// Sima string olvasása - NEM regisztrál az intern táblába.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadPlainString(ref BinaryDeserializationContext context)
{
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
return context.ReadStringUtf8(length);
}
/// <summary>
/// Új internált string olvasása és regisztrálása az intern táblába.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
{
var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty;
var str = context.ReadStringUtf8(length);
context.RegisterInternedString(str);
return str;
}
/// <summary>
/// Read a string and register it in the intern table for future references.
/// </summary>
@ -1109,12 +1134,16 @@ public static class AcBinaryDeserializer
context.Skip(16);
return;
case BinaryTypeCode.String:
// CRITICAL FIX: Must register string in intern table even when skipping!
SkipAndInternString(ref context);
// Sima string - nem regisztrálunk
SkipPlainString(ref context);
return;
case BinaryTypeCode.StringInterned:
context.ReadVarUInt();
return;
case BinaryTypeCode.StringInternNew:
// Új internált string - regisztrálni kell még skip esetén is
SkipAndRegisterInternedString(ref context);
return;
case BinaryTypeCode.ByteArray:
var byteLen = (int)context.ReadVarUInt();
context.Skip(byteLen);
@ -1139,6 +1168,31 @@ public static class AcBinaryDeserializer
}
}
/// <summary>
/// Sima string kihagyása - NEM regisztrál.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipPlainString(ref BinaryDeserializationContext context)
{
var byteLen = (int)context.ReadVarUInt();
if (byteLen > 0)
{
context.Skip(byteLen);
}
}
/// <summary>
/// Új internált string kihagyása - DE regisztrálni kell!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
{
var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return;
var str = context.ReadStringUtf8(byteLen);
context.RegisterInternedString(str);
}
/// <summary>
/// Skip a string but still register it in the intern table if it meets the length threshold.
/// </summary>

View File

@ -29,6 +29,17 @@ public static class AcBinarySerializer
// Pre-computed UTF8 encoder for string operations
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private static readonly Type StringType = typeof(string);
private static readonly Type GuidType = typeof(Guid);
private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
private static readonly Type TimeSpanType = typeof(TimeSpan);
private static readonly Type IntType = typeof(int);
private static readonly Type LongType = typeof(long);
private static readonly Type FloatType = typeof(float);
private static readonly Type DoubleType = typeof(double);
private static readonly Type DecimalType = typeof(decimal);
private static readonly Type BoolType = typeof(bool);
private static readonly Type DateTimeType = typeof(DateTime);
#region Public API
@ -186,6 +197,11 @@ public static class AcBinarySerializer
var metadata = GetTypeMetadata(type);
foreach (var prop in metadata.Properties)
{
if (!context.ShouldSerializeProperty(value, prop))
{
continue;
}
var propValue = prop.GetValue(value);
if (propValue != null)
ScanReferences(propValue, context, depth + 1);
@ -223,6 +239,11 @@ public static class AcBinarySerializer
var metadata = GetTypeMetadata(type);
foreach (var prop in metadata.Properties)
{
if (!context.ShouldIncludePropertyInMetadata(prop))
{
continue;
}
context.RegisterPropertyName(prop.Name);
if (TryResolveNestedMetadataType(prop.PropertyType, out var nestedType))
@ -574,20 +595,28 @@ public static class AcBinarySerializer
return;
}
// Try string interning - but only write ref if already interned
if (context.UseStringInterning && value.Length >= context.MinStringInternLength)
{
if (context.TryGetInternedStringIndex(value, out var index))
{
// Már regisztrált string - csak index
context.WriteByte(BinaryTypeCode.StringInterned);
context.WriteVarUInt((uint)index);
return;
}
// Register for future references
context.RegisterInternedString(value);
if (context.TryPromoteInternCandidate(value, out var promotedIndex))
{
// Második előfordulás - StringInternNew: teljes tartalom + regisztráció
context.WriteByte(BinaryTypeCode.StringInternNew);
context.WriteStringUtf8(value);
return;
}
context.TrackInternCandidate(value);
}
// Write inline string with optimized encoding
// Első előfordulás vagy nincs interning - sima string
context.WriteByte(BinaryTypeCode.String);
context.WriteStringUtf8(value);
}
@ -634,7 +663,9 @@ public static class AcBinarySerializer
for (var i = 0; i < propCount; i++)
{
if (IsPropertyDefaultOrNull(value, properties[i]))
var property = properties[i];
if (!context.ShouldSerializeProperty(value, property) || IsPropertyDefaultOrNull(value, property))
{
propertyStates[i] = 0;
continue;
@ -660,7 +691,7 @@ public static class AcBinarySerializer
}
else
{
WriteString(prop.Name, context);
context.WritePreencodedPropertyName(prop.NameUtf8);
}
WritePropertyValue(value, prop, context, nextDepth);
@ -790,974 +821,6 @@ public static class AcBinarySerializer
#endregion
#region VarInt Encoding (Static Methods for Direct Use)
/// <summary>
/// Write variable-length signed integer (ZigZag encoding).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteVarInt(Span<byte> buffer, int value)
{
// ZigZag encoding
var encoded = (uint)((value << 1) ^ (value >> 31));
return WriteVarUInt(buffer, encoded);
}
/// <summary>
/// Write variable-length unsigned integer.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteVarUInt(Span<byte> buffer, uint value)
{
var i = 0;
while (value >= 0x80)
{
buffer[i++] = (byte)(value | 0x80);
value >>= 7;
}
buffer[i++] = (byte)value;
return i;
}
/// <summary>
/// Write variable-length signed long (ZigZag encoding).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteVarLong(Span<byte> buffer, long value)
{
var encoded = (ulong)((value << 1) ^ (value >> 63));
return WriteVarULong(buffer, encoded);
}
/// <summary>
/// Write variable-length unsigned long.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteVarULong(Span<byte> buffer, ulong value)
{
var i = 0;
while (value >= 0x80)
{
buffer[i++] = (byte)(value | 0x80);
value >>= 7;
}
buffer[i++] = (byte)value;
return i;
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryTypeMetadata GetTypeMetadata(Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDefaultValueFast(object value, TypeCode typeCode, Type propertyType)
{
switch (typeCode)
{
case TypeCode.Int32: return (int)value == 0;
case TypeCode.Int64: return (long)value == 0L;
case TypeCode.Double: return (double)value == 0.0;
case TypeCode.Decimal: return (decimal)value == 0m;
case TypeCode.Single: return (float)value == 0f;
case TypeCode.Byte: return (byte)value == 0;
case TypeCode.Int16: return (short)value == 0;
case TypeCode.UInt16: return (ushort)value == 0;
case TypeCode.UInt32: return (uint)value == 0;
case TypeCode.UInt64: return (ulong)value == 0;
case TypeCode.SByte: return (sbyte)value == 0;
case TypeCode.Boolean: return (bool)value == false;
case TypeCode.String: return string.IsNullOrEmpty((string)value);
}
if (propertyType.IsEnum) return Convert.ToInt32(value) == 0;
if (ReferenceEquals(propertyType, GuidType)) return (Guid)value == Guid.Empty;
return false;
}
internal sealed class BinaryTypeMetadata
{
public BinaryPropertyAccessor[] Properties { get; }
public BinaryTypeMetadata(Type type)
{
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead &&
p.GetIndexParameters().Length == 0 &&
!HasJsonIgnoreAttribute(p))
.Select(p => new BinaryPropertyAccessor(p))
.ToArray();
}
}
/// <summary>
/// Optimized property accessor with typed getters to avoid boxing for common value types.
/// </summary>
internal sealed class BinaryPropertyAccessor
{
public readonly string Name;
public readonly byte[] NameUtf8;
public readonly Type PropertyType;
public readonly TypeCode TypeCode;
// Generic getter (used for reference types and fallback)
private readonly Func<object, object?> _objectGetter;
// Typed getters to avoid boxing - null if not applicable
private readonly Delegate? _typedGetter;
private readonly PropertyAccessorType _accessorType;
public BinaryPropertyAccessor(PropertyInfo prop)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
TypeCode = Type.GetTypeCode(PropertyType);
var declaringType = prop.DeclaringType!;
// Create typed getter for value types to avoid boxing
(_typedGetter, _accessorType) = CreateTypedGetter(declaringType, prop);
// Always create object getter as fallback
_objectGetter = CreateObjectGetter(declaringType, prop);
}
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
{
var propType = prop.PropertyType;
var underlyingType = Nullable.GetUnderlyingType(propType);
var isNullable = underlyingType != null;
var actualType = underlyingType ?? propType;
// For nullable types, we need special handling
if (isNullable)
{
return (null, PropertyAccessorType.Object);
}
// Check enum FIRST before TypeCode (enums have TypeCode.Int32 etc. based on underlying type)
if (actualType.IsEnum)
{
return (CreateEnumGetterDelegate(declaringType, prop), PropertyAccessorType.Enum);
}
// Check for Guid (no TypeCode)
if (ReferenceEquals(actualType, GuidType))
{
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
}
// Create typed getters for common value types
var typeCode = Type.GetTypeCode(actualType);
return typeCode switch
{
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
_ => (null, PropertyAccessorType.Object)
};
}
private static Delegate CreateTypedGetterDelegate<T>(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
return Expression.Lambda<Func<object, T>>(propAccess, objParam).Compile();
}
private static Delegate CreateEnumGetterDelegate(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
// Convert enum to int
var convertToInt = Expression.Convert(propAccess, typeof(int));
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
}
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object obj) => _objectGetter(obj);
/// <summary>
/// Gets the accessor type for optimized writing without boxing.
/// </summary>
public PropertyAccessorType AccessorType => _accessorType;
// Typed getter methods - these avoid boxing
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
}
/// <summary>
/// Identifies the type of property accessor for optimized dispatch.
/// </summary>
internal enum PropertyAccessorType : byte
{
Object = 0,
Int32,
Int64,
Boolean,
Double,
Single,
Decimal,
DateTime,
Byte,
Int16,
UInt16,
UInt32,
UInt64,
Guid,
Enum
}
#endregion
#region Context Pool
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();
}
}
}
#endregion
#region Serialization Context
/// <summary>
/// Optimized serialization context with direct memory operations.
/// Uses ArrayPool for buffer management and MemoryMarshal for zero-copy writes.
/// </summary>
internal sealed class BinarySerializationContext : IDisposable
{
private byte[] _buffer;
private int _position;
private int _initialBufferSize;
// Minimum buffer size for ArrayPool (reduces fragmentation)
private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
// Reference handling
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextRefId;
// String interning
private Dictionary<string, int>? _internedStrings;
private List<string>? _internedStringList;
// Property name table
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 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;
_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;
// Clear collections and trim if they grew too large
// This prevents memory bloat when reusing pooled contexts
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
_internedStringList?.Clear();
_propertyNameList?.Clear();
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;
}
}
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 Optimized Buffer Writing
/// <summary>
/// Ensures buffer has capacity, growing by doubling if needed.
/// </summary>
[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)
{
EnsureCapacity(1);
_buffer[_position++] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteBytes(ReadOnlySpan<byte> data)
{
EnsureCapacity(data.Length);
data.CopyTo(_buffer.AsSpan(_position));
_position += data.Length;
}
/// <summary>
/// Write a blittable value type directly to buffer using MemoryMarshal.
/// </summary>
[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;
}
/// <summary>
/// Optimized decimal writer using GetBits.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteDecimalBits(decimal value)
{
EnsureCapacity(16);
Span<int> bits = stackalloc int[4];
decimal.TryGetBits(value, bits, out _);
var destSpan = _buffer.AsSpan(_position, 16);
MemoryMarshal.AsBytes(bits).CopyTo(destSpan);
_position += 16;
}
/// <summary>
/// Optimized DateTime writer.
/// </summary>
[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;
}
/// <summary>
/// Optimized Guid writer using TryWriteBytes.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteGuidBits(Guid value)
{
EnsureCapacity(16);
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
_position += 16;
}
/// <summary>
/// Optimized DateTimeOffset writer.
/// </summary>
[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;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt(int value)
{
EnsureCapacity(5);
var encoded = (uint)((value << 1) ^ (value >> 31));
WriteVarUIntInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
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)
{
EnsureCapacity(10);
var encoded = (ulong)((value << 1) ^ (value >> 63));
WriteVarULongInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
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;
}
/// <summary>
/// Optimized string writer using span-based UTF8 encoding.
/// Uses stackalloc for small strings to avoid allocations.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteStringUtf8(string value)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
#endregion
#region Bulk Array Writers
/// <summary>
/// Writes int32 array with optimized tiny int encoding.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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);
}
}
}
/// <summary>
/// Writes long array with optimized encoding (falls back to int32 when possible).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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);
}
}
}
/// <summary>
/// Writes double array as bulk raw bytes - most efficient for large arrays.
/// Each double is written with type code prefix for deserializer compatibility.
/// </summary>
public void WriteDoubleArrayBulk(double[] array)
{
// Each double needs 1 byte type code + 8 bytes data = 9 bytes
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;
}
}
/// <summary>
/// Writes float array as bulk raw bytes.
/// </summary>
public void WriteFloatArrayBulk(float[] array)
{
// Each float needs 1 byte type code + 4 bytes data = 5 bytes
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;
}
}
/// <summary>
/// Writes Guid array as bulk raw bytes.
/// </summary>
public void WriteGuidArrayBulk(Guid[] array)
{
// Each Guid needs 1 byte type code + 16 bytes data = 17 bytes
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 Header and Metadata
private int _headerPosition;
public void WriteHeaderPlaceholder()
{
// Reserve space for: version (1) + flags (1)
EnsureCapacity(2);
_headerPosition = _position;
_position += 2;
}
public void WriteMetadata()
{
// Write version at header position
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
// Build flags byte
byte flags = BinaryTypeCode.HeaderFlagsBase;
if (UseMetadata && hasPropertyNames)
{
flags |= BinaryTypeCode.HeaderFlag_Metadata;
}
if (UseReferenceHandling)
{
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
}
_buffer[_headerPosition + 1] = flags;
// Write property names if metadata is enabled
if ((flags & BinaryTypeCode.HeaderFlag_Metadata) != 0)
{
// Write property name count
WriteVarUInt((uint)_propertyNameList!.Count);
// Write property names
foreach (var name in _propertyNameList)
{
WriteStringUtf8(name);
}
}
}
#endregion
#region Reference Handling
// Smaller initial capacity to reduce memory allocation when not many references
private const int InitialReferenceCapacity = 16;
private const int InitialMultiRefCapacity = 8;
[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 String Interning
// Smaller initial capacity for string interning
private const int InitialInternCapacity = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetInternedStringIndex(string value, out int index)
{
if (_internedStrings != null && _internedStrings.TryGetValue(value, out index))
return true;
index = -1;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterInternedString(string value)
{
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
_internedStringList ??= new List<string>(InitialInternCapacity);
if (!_internedStrings.ContainsKey(value))
{
var index = _internedStringList.Count;
_internedStrings[value] = index;
_internedStringList.Add(value);
}
}
#endregion
#region Property Names
// Smaller initial capacity for property names
private const int InitialPropertyNameCapacity = 16;
[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))
{
_propertyNames[name] = _propertyNameList.Count;
_propertyNameList.Add(name);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPropertyNameIndex(string name)
{
return _propertyNames!.TryGetValue(name, out var index) ? index : -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int[] RentPropertyIndexBuffer(int minimumLength)
{
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length >= minimumLength)
{
var buffer = _propertyIndexBuffer;
_propertyIndexBuffer = null;
return buffer;
}
return ArrayPool<int>.Shared.Rent(minimumLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyIndexBuffer(int[] buffer)
{
if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache)
{
_propertyIndexBuffer = buffer;
return;
}
ArrayPool<int>.Shared.Return(buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] RentPropertyStateBuffer(int minimumLength)
{
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= minimumLength)
{
var buffer = _propertyStateBuffer;
_propertyStateBuffer = null;
return buffer;
}
return ArrayPool<byte>.Shared.Rent(minimumLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyStateBuffer(byte[] buffer)
{
if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache)
{
_propertyStateBuffer = buffer;
return;
}
ArrayPool<byte>.Shared.Return(buffer);
}
#endregion
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result);
return result;
}
public BinarySerializationResult DetachResult()
{
var buffer = _buffer;
var length = _position;
var result = new BinarySerializationResult(buffer, length, pooled: true);
var newSize = Math.Max(_initialBufferSize, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(newSize);
_position = 0;
return result;
}
public void WriteTo(IBufferWriter<byte> writer)
{
// Directly write the internal buffer to the IBufferWriter
var span = writer.GetSpan(_position);
_buffer.AsSpan(0, _position).CopyTo(span);
writer.Advance(_position);
}
public int Position => _position;
[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 Serialization Result
public sealed class BinarySerializationResult : IDisposable
@ -1965,4 +1028,984 @@ public static class AcBinarySerializer
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryTypeMetadata GetTypeMetadata(Type type)
=> TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPrimitiveOrStringFast(Type type)
{
if (type.IsPrimitive || ReferenceEquals(type, StringType))
{
return true;
}
if (ReferenceEquals(type, DecimalType) ||
ReferenceEquals(type, DateTimeType) ||
ReferenceEquals(type, GuidType) ||
ReferenceEquals(type, DateTimeOffsetType) ||
ReferenceEquals(type, TimeSpanType))
{
return true;
}
if (type.IsEnum)
{
return true;
}
var underlying = Nullable.GetUnderlyingType(type);
return underlying != null && IsPrimitiveOrStringFast(underlying);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDictionaryType(Type type, out Type? keyType, out Type? valueType)
{
if (type.IsGenericType)
{
var definition = type.GetGenericTypeDefinition();
if (definition == typeof(Dictionary<,>) || definition == typeof(IDictionary<,>))
{
var args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
return true;
}
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IDictionary<,>))
{
var args = iface.GetGenericArguments();
keyType = args[0];
valueType = args[1];
return true;
}
}
keyType = null;
valueType = null;
return typeof(IDictionary).IsAssignableFrom(type);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Type? GetCollectionElementType(Type type)
{
if (type.IsArray)
{
return type.GetElementType();
}
if (type.IsGenericType)
{
var args = type.GetGenericArguments();
if (args.Length == 1)
{
return args[0];
}
}
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return iface.GetGenericArguments()[0];
}
}
return null;
}
internal sealed class BinaryTypeMetadata
{
public BinaryPropertyAccessor[] Properties { get; }
public BinaryTypeMetadata(Type type)
{
Properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead &&
p.GetIndexParameters().Length == 0 &&
!HasJsonIgnoreAttribute(p))
.Select(p => new BinaryPropertyAccessor(p))
.ToArray();
}
}
internal sealed class BinaryPropertyAccessor
{
public readonly string Name;
public readonly byte[] NameUtf8;
public readonly Type PropertyType;
public readonly TypeCode TypeCode;
public readonly Type DeclaringType;
private readonly Func<object, object?> _objectGetter;
private readonly Delegate? _typedGetter;
private readonly PropertyAccessorType _accessorType;
public BinaryPropertyAccessor(PropertyInfo prop)
{
Name = prop.Name;
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
DeclaringType = prop.DeclaringType!;
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
TypeCode = Type.GetTypeCode(PropertyType);
(_typedGetter, _accessorType) = CreateTypedGetter(DeclaringType, prop);
_objectGetter = CreateObjectGetter(DeclaringType, prop);
}
public PropertyAccessorType AccessorType => _accessorType;
public Func<object, object?> ObjectGetter => _objectGetter;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object? GetValue(object obj) => _objectGetter(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
{
var propType = prop.PropertyType;
var underlying = Nullable.GetUnderlyingType(propType);
if (underlying != null)
{
return (null, PropertyAccessorType.Object);
}
if (propType.IsEnum)
{
return (CreateEnumGetter(declaringType, prop), PropertyAccessorType.Enum);
}
if (ReferenceEquals(propType, GuidType))
{
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
}
var typeCode = Type.GetTypeCode(propType);
return typeCode switch
{
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
_ => (null, PropertyAccessorType.Object)
};
}
private static Delegate CreateEnumGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var convertToInt = Expression.Convert(propAccess, typeof(int));
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
}
private static Func<object, TProperty> CreateTypedGetterDelegate<TProperty>(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var convertExpr = Expression.Convert(propAccess, typeof(TProperty));
return Expression.Lambda<Func<object, TProperty>>(convertExpr, objParam).Compile();
}
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var castExpr = Expression.Convert(objParam, declaringType);
var propAccess = Expression.Property(castExpr, prop);
var boxed = Expression.Convert(propAccess, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
}
}
internal enum PropertyAccessorType : byte
{
Object = 0,
Int32,
Int64,
Boolean,
Double,
Single,
Decimal,
DateTime,
Byte,
Int16,
UInt16,
UInt32,
UInt64,
Guid,
Enum
}
#endregion
#region Context Pool
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();
}
}
}
#endregion
#region Serialization Context
internal sealed class BinarySerializationContext : IDisposable
{
private byte[] _buffer;
private int _position;
private int _initialBufferSize;
private const int MinBufferSize = 256;
private const int PropertyIndexBufferMaxCache = 512;
private const int PropertyStateBufferMaxCache = 512;
// Reference handling
private Dictionary<object, int>? _scanOccurrences;
private Dictionary<object, int>? _writtenRefs;
private HashSet<object>? _multiReferenced;
private int _nextRefId;
// String interning
private Dictionary<string, int>? _internedStrings;
private List<string>? _internedStringList;
private HashSet<string>? _internCandidates;
// Property name table
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 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;
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
_internedStringList?.Clear();
_propertyNameList?.Clear();
_internCandidates?.Clear();
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;
}
}
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 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 Optimized Buffer Writing
[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)
{
EnsureCapacity(1);
_buffer[_position++] = value;
}
[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;
}
[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;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarInt(int value)
{
EnsureCapacity(5);
var encoded = (uint)((value << 1) ^ (value >> 31));
WriteVarUIntInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarUInt(uint value)
{
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)
{
EnsureCapacity(10);
var encoded = (ulong)((value << 1) ^ (value >> 63));
WriteVarULongInternal(encoded);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteVarULong(ulong value)
{
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)
{
var byteCount = Utf8NoBom.GetByteCount(value);
WriteVarUInt((uint)byteCount);
EnsureCapacity(byteCount);
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
_position += byteCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
{
WriteByte(BinaryTypeCode.String);
WriteVarUInt((uint)utf8Name.Length);
WriteBytes(utf8Name);
}
#endregion
#region Bulk Array Writers
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 Header and Metadata
private int _headerPosition;
public void WriteHeaderPlaceholder()
{
EnsureCapacity(2);
_headerPosition = _position;
_position += 2;
}
public void WriteMetadata()
{
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
var hasPropertyNames = _propertyNameList != null && _propertyNameList.Count > 0;
byte flags = BinaryTypeCode.HeaderFlagsBase;
if (UseMetadata && hasPropertyNames)
{
flags |= BinaryTypeCode.HeaderFlag_Metadata;
}
if (UseReferenceHandling)
{
flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
}
_buffer[_headerPosition + 1] = flags;
if ((flags & BinaryTypeCode.HeaderFlag_Metadata) != 0)
{
WriteVarUInt((uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
{
WriteStringUtf8(name);
}
}
}
#endregion
#region Reference Handling
private const int InitialReferenceCapacity = 16;
private const int InitialMultiRefCapacity = 8;
[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 String Interning
private const int InitialInternCapacity = 16;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetInternedStringIndex(string value, out int index)
{
if (_internedStrings != null && _internedStrings.TryGetValue(value, out index))
{
return true;
}
index = -1;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterInternedString(string value)
{
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
_internedStringList ??= new List<string>(InitialInternCapacity);
if (!_internedStrings.ContainsKey(value))
{
var index = _internedStringList.Count;
_internedStrings[value] = index;
_internedStringList.Add(value);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void TrackInternCandidate(string value)
{
_internCandidates ??= new HashSet<string>(InitialInternCapacity, StringComparer.Ordinal);
_internCandidates.Add(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPromoteInternCandidate(string value, out int index)
{
if (_internCandidates != null && _internCandidates.Remove(value))
{
RegisterInternedString(value);
return TryGetInternedStringIndex(value, out index);
}
index = -1;
return false;
}
#endregion
#region Property Names
private const int InitialPropertyNameCapacity = 16;
[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))
{
_propertyNames[name] = _propertyNameList.Count;
_propertyNameList.Add(name);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPropertyNameIndex(string name)
{
return _propertyNames!.TryGetValue(name, out var index) ? index : -1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int[] RentPropertyIndexBuffer(int minimumLength)
{
if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length >= minimumLength)
{
var buffer = _propertyIndexBuffer;
_propertyIndexBuffer = null;
return buffer;
}
return ArrayPool<int>.Shared.Rent(minimumLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyIndexBuffer(int[] buffer)
{
if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache)
{
_propertyIndexBuffer = buffer;
return;
}
ArrayPool<int>.Shared.Return(buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte[] RentPropertyStateBuffer(int minimumLength)
{
if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= minimumLength)
{
var buffer = _propertyStateBuffer;
_propertyStateBuffer = null;
return buffer;
}
return ArrayPool<byte>.Shared.Rent(minimumLength);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ReturnPropertyStateBuffer(byte[] buffer)
{
if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache)
{
_propertyStateBuffer = buffer;
return;
}
ArrayPool<byte>.Shared.Return(buffer);
}
#endregion
public byte[] ToArray()
{
var result = GC.AllocateUninitializedArray<byte>(_position);
_buffer.AsSpan(0, _position).CopyTo(result);
return result;
}
public BinarySerializationResult DetachResult()
{
var buffer = _buffer;
var length = _position;
var result = new BinarySerializationResult(buffer, length, pooled: true);
var newSize = Math.Max(_initialBufferSize, MinBufferSize);
_buffer = ArrayPool<byte>.Shared.Rent(newSize);
_position = 0;
return result;
}
public void WriteTo(IBufferWriter<byte> writer)
{
var span = writer.GetSpan(_position);
_buffer.AsSpan(0, _position).CopyTo(span);
writer.Advance(_position);
}
public int Position => _position;
[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
}

View File

@ -1,3 +1,4 @@
using System;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Extensions;
@ -69,6 +70,12 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// </summary>
public int InitialBufferCapacity { get; init; } = 4096;
/// <summary>
/// Optional property-level filter invoked before metadata registration and serialization.
/// Return false to exclude the property from the payload.
/// </summary>
public BinaryPropertyFilter? PropertyFilter { get; init; }
/// <summary>
/// Creates options with specified max depth.
/// </summary>
@ -117,6 +124,7 @@ internal static class BinaryTypeCode
public const byte String = 16; // Inline UTF8 string
public const byte StringInterned = 17; // Reference to interned string by index
public const byte StringEmpty = 18; // Empty string marker
public const byte StringInternNew = 19; // New interned string - full content + register in table
// Date/Time types (20-23)
public const byte DateTime = 20;
@ -190,3 +198,62 @@ internal static class BinaryTypeCode
return false;
}
}
/// <summary>
/// Delegate used to decide whether a property should be serialized.
/// </summary>
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
/// <summary>
/// Provides property metadata and lazy value access for property filter evaluations.
/// </summary>
public readonly struct BinaryPropertyFilterContext
{
private readonly object? _instance;
private readonly Func<object, object?>? _valueGetter;
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
{
_instance = instance;
DeclaringType = declaringType;
PropertyName = propertyName;
PropertyType = propertyType;
_valueGetter = valueGetter;
}
/// <summary>
/// Gets the declaring type of the property.
/// </summary>
public Type DeclaringType { get; }
/// <summary>
/// Gets the property name.
/// </summary>
public string PropertyName { get; }
/// <summary>
/// Gets the property type.
/// </summary>
public Type PropertyType { get; }
/// <summary>
/// Gets the instance being serialized when available. Null during metadata registration.
/// </summary>
public object? Instance => _instance;
/// <summary>
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
/// </summary>
public bool IsMetadataPhase => _instance is null;
/// <summary>
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
/// </summary>
public object? GetValue()
{
if (_instance == null || _valueGetter == null)
return null;
return _valueGetter(_instance);
}
}