From b244d9219a52fda7c0aae85a838180c362d94b33 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 16 Feb 2026 21:26:51 +0100 Subject: [PATCH] Add fast-path support for more primitive collections Extend serializer/deserializer to efficiently handle arrays and lists of short, ushort, uint, ulong, sbyte, char, DateTimeOffset, and TimeSpan. Improve enum collection handling and support ICollection-based collections. Update source generator to distinguish between indexable and counted collections, generating optimal serialization code for each. These changes enhance performance and coverage for a broader range of collection types. --- .../AcBinarySourceGenerator.cs | 35 +++- .../Binaries/AcBinaryDeserializer.cs | 126 +++++++++++++ .../Binaries/AcBinarySerializer.cs | 175 +++++++++++++++++- 3 files changed, 328 insertions(+), 8 deletions(-) diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index e4debc0..188dd2c 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -128,12 +128,26 @@ public class AcBinarySourceGenerator : IIncrementalGenerator elemKind = GetKind(elemType); elemFullTypeName = elemType.ToDisplayString(); - // Detect collection shape: List or T[] + // Detect collection shape for inline write if (p.Type is IArrayTypeSymbol) collKind = "Array"; - else if (p.Type is INamedTypeSymbol collNamedType && - collNamedType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.List") - collKind = "List"; + else if (p.Type is INamedTypeSymbol collNamedType) + { + var origDef = collNamedType.OriginalDefinition.ToDisplayString(); + collKind = origDef switch + { + "System.Collections.Generic.List" => "List", + "System.Collections.Generic.IList" => "List", // has Count + indexer + "System.Collections.Generic.IReadOnlyList" => "List", // has Count + indexer + "System.Collections.Generic.HashSet" => "Counted", // has Count, no indexer + "System.Collections.Generic.Queue" => "Counted", + "System.Collections.Generic.ICollection" => "Counted", + "System.Collections.Generic.IReadOnlyCollection" => "Counted", + "System.Collections.Generic.SortedSet" => "Counted", + "System.Collections.Generic.LinkedList" => "Counted", + _ => null + }; + } // For Complex element types, check for generated writer if (elemKind == PropertyTypeKind.Complex) @@ -450,7 +464,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i}{{"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);"); - // Get count and span/indexer based on collection kind + // Get count and iteration based on collection kind if (p.CollectionKind == "Array") { sb.AppendLine($"{i} var arr_{p.Name} = {a};"); @@ -460,7 +474,16 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];"); } - else // List + else if (p.CollectionKind == "Counted") + { + // HashSet, Queue, ICollection, IReadOnlyCollection, etc. — Count + foreach + sb.AppendLine($"{i} var col_{p.Name} = {a};"); + sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);"); + sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); + sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})"); + sb.AppendLine($"{i} {{"); + } + else // List, IList, IReadOnlyList — Count + indexer { sb.AppendLine($"{i} var list_{p.Name} = {a};"); sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);"); diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 9f0c8fe..2c84df0 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -1458,6 +1458,132 @@ public static partial class AcBinaryDeserializer return array; } + // Float array + if (ReferenceEquals(elementType, FloatType)) + { + var array = new float[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.Float32) return null; + array[i] = context.ReadSingleUnsafe(); + } + + return array; + } + + // Short (Int16) array + if (ReferenceEquals(elementType, ShortType)) + { + var array = new short[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.Int16) return null; + array[i] = context.ReadInt16Unsafe(); + } + + return array; + } + + // UShort (UInt16) array + if (ReferenceEquals(elementType, UShortType)) + { + var array = new ushort[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.UInt16) return null; + array[i] = context.ReadUInt16Unsafe(); + } + + return array; + } + + // UInt32 array + if (ReferenceEquals(elementType, UIntType)) + { + var array = new uint[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.UInt32) return null; + array[i] = context.ReadVarUInt(); + } + + return array; + } + + // UInt64 array + if (ReferenceEquals(elementType, ULongType)) + { + var array = new ulong[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.UInt64) return null; + array[i] = context.ReadVarULong(); + } + + return array; + } + + // SByte (Int8) array + if (ReferenceEquals(elementType, SByteType)) + { + var array = new sbyte[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.Int8) return null; + array[i] = unchecked((sbyte)context.ReadByte()); + } + + return array; + } + + // Char array + if (ReferenceEquals(elementType, CharType)) + { + var array = new char[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.Char) return null; + array[i] = context.ReadCharUnsafe(); + } + + return array; + } + + // DateTimeOffset array + if (ReferenceEquals(elementType, DateTimeOffsetType)) + { + var array = new DateTimeOffset[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.DateTimeOffset) return null; + array[i] = context.ReadDateTimeOffsetUnsafe(); + } + + return array; + } + + // TimeSpan array + if (ReferenceEquals(elementType, TimeSpanType)) + { + var array = new TimeSpan[count]; + for (int i = 0; i < count; i++) + { + var typeCode = context.ReadByte(); + if (typeCode != BinaryTypeCode.TimeSpan) return null; + array[i] = context.ReadTimeSpanUnsafe(); + } + + return array; + } + return null; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 0d0a37f..22041b8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -45,6 +45,13 @@ public static partial class AcBinarySerializer private static readonly Type DecimalType = typeof(decimal); private static readonly Type BoolType = typeof(bool); private static readonly Type DateTimeType = typeof(DateTime); + private static readonly Type ShortType = typeof(short); + private static readonly Type UShortType = typeof(ushort); + private static readonly Type UIntType = typeof(uint); + private static readonly Type ULongType = typeof(ulong); + private static readonly Type ByteType = typeof(byte); + private static readonly Type SByteType = typeof(sbyte); + private static readonly Type CharType = typeof(char); #region Public API @@ -1534,7 +1541,7 @@ public static partial class AcBinarySerializer return; } - // For IList, we can write the count directly + // For IList, we can write the count directly and use indexed access if (enumerable is IList list) { var count = list.Count; @@ -1548,7 +1555,19 @@ public static partial class AcBinarySerializer return; } - // For other IEnumerable, collect first + // For ICollection (HashSet, Queue, SortedSet, etc.) — has Count, no indexer + if (enumerable is ICollection collection) + { + context.WriteVarUInt((uint)collection.Count); + foreach (var item in enumerable) + { + var itemType = item?.GetType() ?? typeof(object); + WriteValue(item, itemType, context, nextDepth); + } + return; + } + + // For other IEnumerable (rare: custom IEnumerable without ICollection), collect first var items = new List(); foreach (var item in enumerable) { @@ -1689,6 +1708,158 @@ public static partial class AcBinarySerializer return true; } + if (ReferenceEquals(elementType, ShortType)) + { + ReadOnlySpan span; + if (enumerable is short[] 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++) + WriteInt16Unsafe(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, UShortType)) + { + ReadOnlySpan span; + if (enumerable is ushort[] 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++) + WriteUInt16Unsafe(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, UIntType)) + { + ReadOnlySpan span; + if (enumerable is uint[] 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++) + WriteUInt32(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, ULongType)) + { + ReadOnlySpan span; + if (enumerable is ulong[] 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++) + WriteUInt64(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, ByteType)) + { + // Note: top-level byte[] goes through WriteByteArray (BinaryTypeCode.ByteArray). + // This handles List and byte[] when reached via WriteArray (element-level encoding). + ReadOnlySpan span; + if (enumerable is byte[] 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(BinaryTypeCode.UInt8); + context.WriteByte(span[i]); + } + return true; + } + + if (ReferenceEquals(elementType, SByteType)) + { + ReadOnlySpan span; + if (enumerable is sbyte[] 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(BinaryTypeCode.Int8); + context.WriteByte(unchecked((byte)span[i])); + } + return true; + } + + if (ReferenceEquals(elementType, CharType)) + { + ReadOnlySpan span; + if (enumerable is char[] 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++) + WriteCharUnsafe(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, DateTimeOffsetType)) + { + ReadOnlySpan span; + if (enumerable is DateTimeOffset[] 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++) + WriteDateTimeOffsetUnsafe(span[i], context); + return true; + } + + if (ReferenceEquals(elementType, TimeSpanType)) + { + ReadOnlySpan span; + if (enumerable is TimeSpan[] 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++) + WriteTimeSpanUnsafe(span[i], context); + return true; + } + + // Enum collections: iterate without GetWrapper/WriteValue dispatch + if (elementType.IsEnum) + { + if (enumerable is IList enumList) + { + context.WriteVarUInt((uint)enumList.Count); + for (var i = 0; i < enumList.Count; i++) + WriteEnum(enumList[i]!, context); + } + else if (enumerable is ICollection enumCol) + { + context.WriteVarUInt((uint)enumCol.Count); + foreach (var item in enumerable) + WriteEnum(item!, context); + } + else + { + var items = new List(); + foreach (var item in enumerable) + items.Add(item!); + context.WriteVarUInt((uint)items.Count); + for (var i = 0; i < items.Count; i++) + WriteEnum(items[i], context); + } + return true; + } + return false; }