From 271f23d0f62eb4234e2b528a989a4940eaa4d8ee Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 14 Dec 2025 12:45:29 +0100 Subject: [PATCH] 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 --- AyCode.Benchmark/SerializationBenchmarks.cs | 76 + .../Extensions/AcBinaryDeserializer.cs | 60 +- AyCode.Core/Extensions/AcBinarySerializer.cs | 1991 +++++++++-------- .../Extensions/AcBinarySerializerOptions.cs | 67 + 4 files changed, 1217 insertions(+), 977 deletions(-) diff --git a/AyCode.Benchmark/SerializationBenchmarks.cs b/AyCode.Benchmark/SerializationBenchmarks.cs index 3ec6494..0d4901c 100644 --- a/AyCode.Benchmark/SerializationBenchmarks.cs +++ b/AyCode.Benchmark/SerializationBenchmarks.cs @@ -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(MsgPackData, MsgPackOptions); + + [Benchmark(Description = "AcBinary Deserialize")] + public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize(AcBinaryData); } \ No newline at end of file diff --git a/AyCode.Core/Extensions/AcBinaryDeserializer.cs b/AyCode.Core/Extensions/AcBinaryDeserializer.cs index 07172cf..23775d3 100644 --- a/AyCode.Core/Extensions/AcBinaryDeserializer.cs +++ b/AyCode.Core/Extensions/AcBinaryDeserializer.cs @@ -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); } + /// + /// Sima string olvasása - NEM regisztrál az intern táblába. + /// + [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); + } + + /// + /// Új internált string olvasása és regisztrálása az intern táblába. + /// + [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; + } + /// /// Read a string and register it in the intern table for future references. /// @@ -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 } } + /// + /// Sima string kihagyása - NEM regisztrál. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SkipPlainString(ref BinaryDeserializationContext context) + { + var byteLen = (int)context.ReadVarUInt(); + if (byteLen > 0) + { + context.Skip(byteLen); + } + } + + /// + /// Új internált string kihagyása - DE regisztrálni kell! + /// + [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); + } + /// /// Skip a string but still register it in the intern table if it meets the length threshold. /// diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs index 562336d..95188ac 100644 --- a/AyCode.Core/Extensions/AcBinarySerializer.cs +++ b/AyCode.Core/Extensions/AcBinarySerializer.cs @@ -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) - - /// - /// Write variable-length signed integer (ZigZag encoding). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteVarInt(Span buffer, int value) - { - // ZigZag encoding - var encoded = (uint)((value << 1) ^ (value >> 31)); - return WriteVarUInt(buffer, encoded); - } - - /// - /// Write variable-length unsigned integer. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteVarUInt(Span buffer, uint value) - { - var i = 0; - while (value >= 0x80) - { - buffer[i++] = (byte)(value | 0x80); - value >>= 7; - } - buffer[i++] = (byte)value; - return i; - } - - /// - /// Write variable-length signed long (ZigZag encoding). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteVarLong(Span buffer, long value) - { - var encoded = (ulong)((value << 1) ^ (value >> 63)); - return WriteVarULong(buffer, encoded); - } - - /// - /// Write variable-length unsigned long. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int WriteVarULong(Span 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(); - } - } - - /// - /// Optimized property accessor with typed getters to avoid boxing for common value types. - /// - 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 _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(declaringType, prop), PropertyAccessorType.Guid); - } - - // Create typed getters for common value types - var typeCode = Type.GetTypeCode(actualType); - return typeCode switch - { - TypeCode.Int32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int32), - TypeCode.Int64 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int64), - TypeCode.Boolean => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Boolean), - TypeCode.Double => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Double), - TypeCode.Single => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Single), - TypeCode.Decimal => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Decimal), - TypeCode.DateTime => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.DateTime), - TypeCode.Byte => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Byte), - TypeCode.Int16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int16), - TypeCode.UInt16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt16), - TypeCode.UInt32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt32), - TypeCode.UInt64 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt64), - _ => (null, PropertyAccessorType.Object) - }; - } - - private static Delegate CreateTypedGetterDelegate(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>(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>(convertToInt, objParam).Compile(); - } - - private static Func 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>(boxed, objParam).Compile(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object obj) => _objectGetter(obj); - - /// - /// Gets the accessor type for optimized writing without boxing. - /// - public PropertyAccessorType AccessorType => _accessorType; - - // Typed getter methods - these avoid boxing - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetInt32(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetInt64(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool GetBoolean(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public double GetDouble(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float GetSingle(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public decimal GetDecimal(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTime GetDateTime(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public byte GetByte(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public short GetInt16(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort GetUInt16(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint GetUInt32(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ulong GetUInt64(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Guid GetGuid(object obj) => ((Func)_typedGetter!)(obj); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetEnumAsInt32(object obj) => ((Func)_typedGetter!)(obj); - } - - /// - /// Identifies the type of property accessor for optimized dispatch. - /// - 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 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 - - /// - /// Optimized serialization context with direct memory operations. - /// Uses ArrayPool for buffer management and MemoryMarshal for zero-copy writes. - /// - 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? _scanOccurrences; - private Dictionary? _writtenRefs; - private HashSet? _multiReferenced; - private int _nextRefId; - - // String interning - private Dictionary? _internedStrings; - private List? _internedStringList; - - // Property name table - private Dictionary? _propertyNames; - private List? _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.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.Shared.Return(_buffer); - _buffer = ArrayPool.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.Shared.Return(_propertyIndexBuffer); - _propertyIndexBuffer = null; - } - - if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache) - { - ArrayPool.Shared.Return(_propertyStateBuffer); - _propertyStateBuffer = null; - } - } - - public void Dispose() - { - if (_buffer != null) - { - ArrayPool.Shared.Return(_buffer); - _buffer = null!; - } - - if (_propertyIndexBuffer != null) - { - ArrayPool.Shared.Return(_propertyIndexBuffer); - _propertyIndexBuffer = null; - } - - if (_propertyStateBuffer != null) - { - ArrayPool.Shared.Return(_propertyStateBuffer); - _propertyStateBuffer = null; - } - } - - #region Optimized Buffer Writing - - /// - /// Ensures buffer has capacity, growing by doubling if needed. - /// - [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.Shared.Rent(newSize); - _buffer.AsSpan(0, _position).CopyTo(newBuffer); - ArrayPool.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 data) - { - EnsureCapacity(data.Length); - data.CopyTo(_buffer.AsSpan(_position)); - _position += data.Length; - } - - /// - /// Write a blittable value type directly to buffer using MemoryMarshal. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteRaw(T value) where T : unmanaged - { - var size = Unsafe.SizeOf(); - EnsureCapacity(size); - Unsafe.WriteUnaligned(ref _buffer[_position], value); - _position += size; - } - - /// - /// Optimized decimal writer using GetBits. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteDecimalBits(decimal value) - { - EnsureCapacity(16); - Span bits = stackalloc int[4]; - decimal.TryGetBits(value, bits, out _); - - var destSpan = _buffer.AsSpan(_position, 16); - MemoryMarshal.AsBytes(bits).CopyTo(destSpan); - _position += 16; - } - - /// - /// Optimized DateTime writer. - /// - [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; - } - - /// - /// Optimized Guid writer using TryWriteBytes. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void WriteGuidBits(Guid value) - { - EnsureCapacity(16); - value.TryWriteBytes(_buffer.AsSpan(_position, 16)); - _position += 16; - } - - /// - /// Optimized DateTimeOffset writer. - /// - [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; - } - - /// - /// Optimized string writer using span-based UTF8 encoding. - /// Uses stackalloc for small strings to avoid allocations. - /// - [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 - - /// - /// Writes int32 array with optimized tiny int encoding. - /// - [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); - } - } - } - - /// - /// Writes long array with optimized encoding (falls back to int32 when possible). - /// - [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); - } - } - } - - /// - /// Writes double array as bulk raw bytes - most efficient for large arrays. - /// Each double is written with type code prefix for deserializer compatibility. - /// - 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; - } - } - - /// - /// Writes float array as bulk raw bytes. - /// - 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; - } - } - - /// - /// Writes Guid array as bulk raw bytes. - /// - 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(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); - _multiReferenced ??= new HashSet(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(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(InitialInternCapacity, StringComparer.Ordinal); - _internedStringList ??= new List(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(InitialPropertyNameCapacity, StringComparer.Ordinal); - _propertyNameList ??= new List(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.Shared.Rent(minimumLength); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReturnPropertyIndexBuffer(int[] buffer) - { - if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache) - { - _propertyIndexBuffer = buffer; - return; - } - - ArrayPool.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.Shared.Rent(minimumLength); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ReturnPropertyStateBuffer(byte[] buffer) - { - if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache) - { - _propertyStateBuffer = buffer; - return; - } - - ArrayPool.Shared.Return(buffer); - } - - #endregion - - public byte[] ToArray() - { - var result = GC.AllocateUninitializedArray(_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.Shared.Rent(newSize); - _position = 0; - - return result; - } - - public void WriteTo(IBufferWriter 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(Dictionary? 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(HashSet? 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 _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 ObjectGetter => _objectGetter; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object? GetValue(object obj) => _objectGetter(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetInt32(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetInt64(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetBoolean(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetDouble(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float GetSingle(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public decimal GetDecimal(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public DateTime GetDateTime(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte GetByte(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short GetInt16(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort GetUInt16(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint GetUInt32(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong GetUInt64(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Guid GetGuid(object obj) => ((Func)_typedGetter!)(obj); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetEnumAsInt32(object obj) => ((Func)_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(declaringType, prop), PropertyAccessorType.Guid); + } + + var typeCode = Type.GetTypeCode(propType); + return typeCode switch + { + TypeCode.Int32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int32), + TypeCode.Int64 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int64), + TypeCode.Boolean => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Boolean), + TypeCode.Double => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Double), + TypeCode.Single => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Single), + TypeCode.Decimal => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Decimal), + TypeCode.DateTime => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.DateTime), + TypeCode.Byte => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Byte), + TypeCode.Int16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.Int16), + TypeCode.UInt16 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt16), + TypeCode.UInt32 => (CreateTypedGetterDelegate(declaringType, prop), PropertyAccessorType.UInt32), + TypeCode.UInt64 => (CreateTypedGetterDelegate(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>(convertToInt, objParam).Compile(); + } + + private static Func CreateTypedGetterDelegate(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>(convertExpr, objParam).Compile(); + } + + private static Func 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>(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 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? _scanOccurrences; + private Dictionary? _writtenRefs; + private HashSet? _multiReferenced; + private int _nextRefId; + + // String interning + private Dictionary? _internedStrings; + private List? _internedStringList; + private HashSet? _internCandidates; + + // Property name table + private Dictionary? _propertyNames; + private List? _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.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.Shared.Return(_buffer); + _buffer = ArrayPool.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.Shared.Return(_propertyIndexBuffer); + _propertyIndexBuffer = null; + } + + if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache) + { + ArrayPool.Shared.Return(_propertyStateBuffer); + _propertyStateBuffer = null; + } + } + + public void Dispose() + { + if (_buffer != null) + { + ArrayPool.Shared.Return(_buffer); + _buffer = null!; + } + + if (_propertyIndexBuffer != null) + { + ArrayPool.Shared.Return(_propertyIndexBuffer); + _propertyIndexBuffer = null; + } + + if (_propertyStateBuffer != null) + { + ArrayPool.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.Shared.Rent(newSize); + _buffer.AsSpan(0, _position).CopyTo(newBuffer); + ArrayPool.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 data) + { + EnsureCapacity(data.Length); + data.CopyTo(_buffer.AsSpan(_position)); + _position += data.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteRaw(T value) where T : unmanaged + { + var size = Unsafe.SizeOf(); + EnsureCapacity(size); + Unsafe.WriteUnaligned(ref _buffer[_position], value); + _position += size; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteDecimalBits(decimal value) + { + EnsureCapacity(16); + Span 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 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(InitialReferenceCapacity, ReferenceEqualityComparer.Instance); + _multiReferenced ??= new HashSet(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(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(InitialInternCapacity, StringComparer.Ordinal); + _internedStringList ??= new List(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(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(InitialPropertyNameCapacity, StringComparer.Ordinal); + _propertyNameList ??= new List(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.Shared.Rent(minimumLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnPropertyIndexBuffer(int[] buffer) + { + if (_propertyIndexBuffer == null && buffer.Length <= PropertyIndexBufferMaxCache) + { + _propertyIndexBuffer = buffer; + return; + } + + ArrayPool.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.Shared.Rent(minimumLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReturnPropertyStateBuffer(byte[] buffer) + { + if (_propertyStateBuffer == null && buffer.Length <= PropertyStateBufferMaxCache) + { + _propertyStateBuffer = buffer; + return; + } + + ArrayPool.Shared.Return(buffer); + } + + #endregion + + public byte[] ToArray() + { + var result = GC.AllocateUninitializedArray(_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.Shared.Rent(newSize); + _position = 0; + + return result; + } + + public void WriteTo(IBufferWriter 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(Dictionary? 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(HashSet? set, int maxCapacity) + { + if (set == null) + { + return; + } + + set.Clear(); + if (set.EnsureCapacity(0) > maxCapacity) + { + set.TrimExcess(); + } + } + } + + #endregion + } \ No newline at end of file diff --git a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs b/AyCode.Core/Extensions/AcBinarySerializerOptions.cs index 96d722e..9b4de01 100644 --- a/AyCode.Core/Extensions/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Extensions/AcBinarySerializerOptions.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.CompilerServices; namespace AyCode.Core.Extensions; @@ -69,6 +70,12 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// public int InitialBufferCapacity { get; init; } = 4096; + /// + /// Optional property-level filter invoked before metadata registration and serialization. + /// Return false to exclude the property from the payload. + /// + public BinaryPropertyFilter? PropertyFilter { get; init; } + /// /// Creates options with specified max depth. /// @@ -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; } } + +/// +/// Delegate used to decide whether a property should be serialized. +/// +public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context); + +/// +/// Provides property metadata and lazy value access for property filter evaluations. +/// +public readonly struct BinaryPropertyFilterContext +{ + private readonly object? _instance; + private readonly Func? _valueGetter; + + internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func? valueGetter) + { + _instance = instance; + DeclaringType = declaringType; + PropertyName = propertyName; + PropertyType = propertyType; + _valueGetter = valueGetter; + } + + /// + /// Gets the declaring type of the property. + /// + public Type DeclaringType { get; } + + /// + /// Gets the property name. + /// + public string PropertyName { get; } + + /// + /// Gets the property type. + /// + public Type PropertyType { get; } + + /// + /// Gets the instance being serialized when available. Null during metadata registration. + /// + public object? Instance => _instance; + + /// + /// Indicates whether the filter is invoked during metadata registration (when no instance is available). + /// + public bool IsMetadataPhase => _instance is null; + + /// + /// Lazily obtains the current property value. Returns null when invoked during metadata registration. + /// + public object? GetValue() + { + if (_instance == null || _valueGetter == null) + return null; + + return _valueGetter(_instance); + } +}