From 7284856dda0a29c43f31164c53e7fb76926b1825 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 16 Feb 2026 13:18:13 +0100 Subject: [PATCH] Unify and optimize primitive array/list serialization Refactor AcBinarySerializer to use ReadOnlySpan for bulk writing of primitive arrays and List, replacing multiple specialized methods with a single TryWritePrimitiveCollection. This improves efficiency and reduces code duplication. Change default string interning mode to Attribute (opt-in). Update generated code path to allow reference tracking but not string interning. Adjust benchmarks to test correct serializer options. Reorder options for clarity. --- AyCode.Core.Serializers.Console/Program.cs | 16 +- ...rySerializer.BinarySerializationContext.cs | 36 +-- .../Binaries/AcBinarySerializer.cs | 217 ++++++++++-------- .../Binaries/AcBinarySerializerOptions.cs | 18 +- 4 files changed, 155 insertions(+), 132 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 9900348..3976efa 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -212,15 +212,15 @@ public static class Program { // AcBinary variants - //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), - //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef), - //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), - //new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern), - - new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault), - new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef), + new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), + new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), - new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern), + new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern), + + //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault), + //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef), + //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), + //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern), // MemoryPack new MemoryPackBenchmark(testData.Order, SerializerMemoryPack), diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 16c155e..5cd4e8e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -558,44 +558,44 @@ public static partial class AcBinarySerializer #region Bulk Array Writes — inline - public void WriteDoubleArrayBulk(double[] array) + public void WriteDoubleArrayBulk(ReadOnlySpan span) { - EnsureCapacity(array.Length * 9); - for (var i = 0; i < array.Length; i++) + EnsureCapacity(span.Length * 9); + for (var i = 0; i < span.Length; i++) { _buffer[_position++] = BinaryTypeCode.Float64; - Unsafe.WriteUnaligned(ref _buffer[_position], array[i]); + Unsafe.WriteUnaligned(ref _buffer[_position], span[i]); _position += 8; } } - public void WriteFloatArrayBulk(float[] array) + public void WriteFloatArrayBulk(ReadOnlySpan span) { - EnsureCapacity(array.Length * 5); - for (var i = 0; i < array.Length; i++) + EnsureCapacity(span.Length * 5); + for (var i = 0; i < span.Length; i++) { _buffer[_position++] = BinaryTypeCode.Float32; - Unsafe.WriteUnaligned(ref _buffer[_position], array[i]); + Unsafe.WriteUnaligned(ref _buffer[_position], span[i]); _position += 4; } } - public void WriteGuidArrayBulk(Guid[] array) + public void WriteGuidArrayBulk(ReadOnlySpan span) { - EnsureCapacity(array.Length * 17); - for (var i = 0; i < array.Length; i++) + EnsureCapacity(span.Length * 17); + for (var i = 0; i < span.Length; i++) { _buffer[_position++] = BinaryTypeCode.Guid; - array[i].TryWriteBytes(_buffer.AsSpan(_position, 16)); + span[i].TryWriteBytes(_buffer.AsSpan(_position, 16)); _position += 16; } } - public void WriteInt32ArrayOptimized(int[] array) + public void WriteInt32ArrayOptimized(ReadOnlySpan span) { - for (var i = 0; i < array.Length; i++) + for (var i = 0; i < span.Length; i++) { - var value = array[i]; + var value = span[i]; if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny)) { WriteByte(tiny); @@ -608,11 +608,11 @@ public static partial class AcBinarySerializer } } - public void WriteLongArrayOptimized(long[] array) + public void WriteLongArrayOptimized(ReadOnlySpan span) { - for (var i = 0; i < array.Length; i++) + for (var i = 0; i < span.Length; i++) { - var value = array[i]; + var value = span[i]; if (value >= int.MinValue && value <= int.MaxValue) { var intValue = (int)value; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 8e3fb30..807a3e5 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1047,12 +1047,13 @@ public static partial class AcBinarySerializer var hasPropertyFilter = context.HasPropertyFilter; // Source-generated fast path: bypass the entire switch/delegate loop. - // Only when no caching features are active (no string interning, no reference handling) - // to avoid scan pass / write pass mismatch with interned strings and tracked references. + // Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties) + // and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking. + // String interning is NOT safe: generated code doesn't set StringInternEligible → cursor mismatch. if (context.UseGeneratedCode) { var generatedWriter = wrapper.GeneratedWriter; - if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching) + if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.UseStringInterning) { generatedWriter.WriteProperties(value, context, nextDepth); return; @@ -1493,7 +1494,7 @@ public static partial class AcBinarySerializer #region Specialized Array Writers /// - /// Optimized array writer with specialized paths for primitive arrays. + /// Optimized array writer with specialized paths for primitive collections. /// private static void WriteArray(IEnumerable enumerable, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) where TOutput : struct, IBinaryOutputBase @@ -1505,10 +1506,10 @@ public static partial class AcBinarySerializer var metadata = wrapper.Metadata; var elementType = metadata.CollectionElementType; - // Optimized path for primitive arrays - if (elementType != null && metadata.SourceType.IsArray) + // Optimized path for primitive T[] and List — unified via ReadOnlySpan + if (elementType != null) { - if (TryWritePrimitiveArray(enumerable, elementType, context)) + if (TryWritePrimitiveCollection(enumerable, elementType, context)) return; } @@ -1542,18 +1543,123 @@ public static partial class AcBinarySerializer } /// - /// Specialized array writer for primitive arrays using bulk memory operations. + /// Unified primitive collection writer for both T[] and List<T>. + /// Extracts ReadOnlySpan<T> from either source (zero-cost implicit conversion for arrays, + /// CollectionsMarshal.AsSpan for lists) and writes using bulk context methods with EnsureCapacity. /// - private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context) + private static bool TryWritePrimitiveCollection(IEnumerable enumerable, Type elementType, BinarySerializationContext context) where TOutput : struct, IBinaryOutputBase { - // String array needs context for interning — keep generic path - if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray) + if (ReferenceEquals(elementType, IntType)) { - context.WriteVarUInt((uint)stringArray.Length); - for (var i = 0; i < stringArray.Length; i++) + ReadOnlySpan span; + if (enumerable is int[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + context.WriteInt32ArrayOptimized(span); + return true; + } + + if (ReferenceEquals(elementType, LongType)) + { + ReadOnlySpan span; + if (enumerable is long[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + context.WriteLongArrayOptimized(span); + return true; + } + + if (ReferenceEquals(elementType, DoubleType)) + { + ReadOnlySpan span; + if (enumerable is double[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + context.WriteDoubleArrayBulk(span); + return true; + } + + if (ReferenceEquals(elementType, FloatType)) + { + ReadOnlySpan span; + if (enumerable is float[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + context.WriteFloatArrayBulk(span); + return true; + } + + if (ReferenceEquals(elementType, BoolType)) + { + ReadOnlySpan span; + if (enumerable is bool[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + for (var i = 0; i < span.Length; i++) + context.WriteByte(span[i] ? BinaryTypeCode.True : BinaryTypeCode.False); + return true; + } + + if (ReferenceEquals(elementType, GuidType)) + { + ReadOnlySpan span; + if (enumerable is Guid[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + context.WriteGuidArrayBulk(span); + return true; + } + + if (ReferenceEquals(elementType, DecimalType)) + { + ReadOnlySpan span; + if (enumerable is decimal[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + for (var i = 0; i < span.Length; i++) + WriteDecimalUnsafe(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, DateTimeType)) + { + ReadOnlySpan span; + if (enumerable is DateTime[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + for (var i = 0; i < span.Length; i++) + WriteDateTimeUnsafe(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, StringType)) + { + ReadOnlySpan span; + if (enumerable is string?[] arr) span = arr; + else if (enumerable is List list) span = CollectionsMarshal.AsSpan(list); + else return false; + + context.WriteVarUInt((uint)span.Length); + for (var i = 0; i < span.Length; i++) { - var s = stringArray[i]; + var s = span[i]; if (s == null) context.WriteByte(BinaryTypeCode.Null); else @@ -1562,89 +1668,6 @@ public static partial class AcBinarySerializer return true; } - // All other primitive arrays — inline write through context (zero virtual dispatch) - return TryWritePrimitiveArrayCore(enumerable, elementType, context); - } - - /// - /// Core primitive array writes. Uses context write methods (zero virtual dispatch). - /// - private static bool TryWritePrimitiveArrayCore(IEnumerable enumerable, Type elementType, BinarySerializationContext context) - where TOutput : struct, IBinaryOutputBase - { - // 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; - } - return false; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index a41a6df..654d25e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -86,14 +86,6 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions public bool UseGeneratedCode { get; set; } = true; - /// - /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). - /// Throws exception if FNV-1a hash collision is detected between property names of the same type. - /// Should be enabled during development/testing, can be disabled in production for performance. - /// Default: true (safety first) - /// - public bool CheckDuplicatePropName { get; init; } = true; - /// /// Controls how string interning is applied during serialization. /// None: No interning, all strings written inline. @@ -101,7 +93,15 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// All: All strings within length limits are interned (legacy behavior). /// Default: All /// - public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All; + public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute; + + /// + /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). + /// Throws exception if FNV-1a hash collision is detected between property names of the same type. + /// Should be enabled during development/testing, can be disabled in production for performance. + /// Default: true (safety first) + /// + public bool CheckDuplicatePropName { get; init; } = true; /// /// Minimum string length to consider for interning.