Optimize collection serialization for complex element types

Enhance AcBinarySourceGenerator to emit direct write loops for List<T>/T[] where T is a complex type with a generated writer. This bypasses per-element runtime dispatch, improving serialization performance. PropInfo now tracks collection element metadata, enabling this optimization. Falls back to generic handling for other cases.
This commit is contained in:
Loretta 2026-02-16 19:37:02 +01:00
parent 9973b6be12
commit 7977feb36a
1 changed files with 199 additions and 6 deletions

View File

@ -113,13 +113,60 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
}
}
// Collection element type analysis for inline collection write
PropertyTypeKind elemKind = PropertyTypeKind.Unknown;
bool elemHasGenWriter = false;
bool elemIsIId = false;
string? elemWriterClassName = null;
string? collKind = null;
string? elemFullTypeName = null;
if (kind == PropertyTypeKind.Collection)
{
var elemType = GetCollectionElementType(p.Type);
if (elemType != null)
{
elemKind = GetKind(elemType);
elemFullTypeName = elemType.ToDisplayString();
// Detect collection shape: List<T> or T[]
if (p.Type is IArrayTypeSymbol)
collKind = "Array";
else if (p.Type is INamedTypeSymbol collNamedType &&
collNamedType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.List<T>")
collKind = "List";
// For Complex element types, check for generated writer
if (elemKind == PropertyTypeKind.Complex)
{
var resolvedElem = elemType is INamedTypeSymbol namedElem
? namedElem.OriginalDefinition : elemType;
elemHasGenWriter = resolvedElem.GetAttributes().Any(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
if (elemHasGenWriter && resolvedElem is INamedTypeSymbol nte && nte.ContainingType != null)
elemHasGenWriter = false;
if (elemHasGenWriter)
{
elemIsIId = resolvedElem.AllInterfaces.Any(ifc =>
ifc.IsGenericType &&
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
var ens = resolvedElem.ContainingNamespace.IsGlobalNamespace
? string.Empty : resolvedElem.ContainingNamespace.ToDisplayString();
elemWriterClassName = string.IsNullOrEmpty(ens)
? $"{resolvedElem.Name}_GeneratedWriter"
: $"{ens}.{resolvedElem.Name}_GeneratedWriter";
}
}
}
}
properties.Add(new PropInfo(
p.Name,
typeDisplayName,
typeNameForTypeof,
kind,
p.Type.NullableAnnotation == NullableAnnotation.Annotated || IsNullableVT(p.Type),
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName));
stringInternAttr, hasGenWriter, propTypeIsIId, writerClassName,
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, collKind, elemFullTypeName));
}
}
@ -161,8 +208,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.CompilerServices;");
sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
// ReferenceHandlingMode is needed when any Complex property has direct object write
if (ci.Properties.Any(p => p.HasGeneratedWriter))
// ReferenceHandlingMode is needed when any Complex/Collection property has direct object write
if (ci.Properties.Any(p => p.HasGeneratedWriter || p.ElementHasGeneratedWriter))
sb.AppendLine("using AyCode.Core.Serializers;");
sb.AppendLine();
if (!string.IsNullOrEmpty(ci.Namespace))
@ -234,8 +281,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i}AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
break;
case PropertyTypeKind.Collection:
// typeof() instead of GetType() — avoids virtual dispatch
if (p.IsNullable)
// Direct collection write for List<T>/T[] with Complex element types that have generated writers
if (p.ElementHasGeneratedWriter && p.CollectionKind != null)
EmitDirectCollectionWrite(sb, p, a, i);
else if (p.IsNullable)
{
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
@ -377,6 +426,105 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
}
}
/// <summary>
/// Emits inline collection write for List&lt;T&gt; / T[] where T is a Complex type with generated writer.
/// Bypasses GetWrapper + WriteArray + WriteValue per-element dispatch entirely.
/// Wire format: [Array marker][VarUInt count][elem₁ marker+props][elem₂ marker+props]...
/// Falls back to WriteValueGenerated when context.IsDirectObjectWrite is false.
/// </summary>
private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i)
{
var writer = p.ElementWriterClassName;
var elemType = p.ElementFullTypeName;
if (p.IsNullable)
{
sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
sb.AppendLine($"{i}else if (context.IsDirectObjectWrite)");
}
else
{
sb.AppendLine($"{i}if (context.IsDirectObjectWrite)");
}
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Array);");
// Get count and span/indexer based on collection kind
if (p.CollectionKind == "Array")
{
sb.AppendLine($"{i} var arr_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)arr_{p.Name}.Length);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < arr_{p.Name}.Length; i_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];");
}
else // List
{
sb.AppendLine($"{i} var list_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < list_{p.Name}.Count; i_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];");
}
// Per-element write — same logic as EmitDirectObjectWrite but for each element
var e = $"elem_{p.Name}";
// Elements in a collection can be null (runtime writes Null marker)
sb.AppendLine($"{i} if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
sb.AppendLine($"{i} if (nextDepth_{p.Name} > context.MaxDepth) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");
if (p.ElementIsIId)
{
sb.AppendLine($"{i} if (context.ReferenceHandling != ReferenceHandlingMode.None && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRef);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectRefFirst);");
sb.AppendLine($"{i} context.WriteVarUInt((uint)epe_{p.Name}.CacheMapIndex);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} }}");
}
else
{
// Non-IId element: guard against ReferenceHandling.All (cursor alignment)
sb.AppendLine($"{i} if (context.ReferenceHandling == ReferenceHandlingMode.All)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} AcBinarySerializer.WriteObjectGenerated({e}, typeof({elemType}), context, nextDepth_{p.Name} - 1);");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} else");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.Object);");
sb.AppendLine($"{i} {writer}.Instance.WriteProperties({e}, context, nextDepth_{p.Name});");
sb.AppendLine($"{i} }}");
}
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
// Fallback for non-direct mode
if (p.IsNullable)
sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
else
{
sb.AppendLine($"{i}else");
sb.AppendLine($"{i} AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context, depth);");
}
}
private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i)
{
switch (k)
@ -531,6 +679,29 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
return PropertyTypeKind.Unknown;
}
/// <summary>
/// Extracts the element type T from List&lt;T&gt;, T[], IList&lt;T&gt;, IEnumerable&lt;T&gt;.
/// Returns null if the element type cannot be determined.
/// </summary>
private static ITypeSymbol? GetCollectionElementType(ITypeSymbol type)
{
// T[] → element type
if (type is IArrayTypeSymbol arrayType)
return arrayType.ElementType;
// Generic collections: List<T>, IList<T>, ICollection<T>, IEnumerable<T>
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
{
// Direct: List<T>, HashSet<T>, etc. — first type argument
var iface = namedType.AllInterfaces
.FirstOrDefault(i => i.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T);
if (iface != null)
return iface.TypeArguments[0];
}
return null;
}
private static bool IsNullableVTKind(PropertyTypeKind k) => k >= PropertyTypeKind.NullableInt32;
private static PropertyTypeKind Underlying(PropertyTypeKind k) => k switch
@ -584,8 +755,24 @@ internal sealed class PropInfo
/// <summary>Generated writer class name, e.g. "SharedTag_GeneratedWriter". Only set when HasGeneratedWriter.</summary>
public string? WriterClassName { get; }
// Collection element metadata — set when TypeKind == Collection and element type is Complex with generated writer
/// <summary>Element type kind for collection properties. Only meaningful when TypeKind == Collection.</summary>
public PropertyTypeKind ElementKind { get; }
/// <summary>True if collection element type has [AcBinarySerializable].</summary>
public bool ElementHasGeneratedWriter { get; }
/// <summary>True if collection element type implements IId&lt;T&gt;.</summary>
public bool ElementIsIId { get; }
/// <summary>Generated writer class name for collection element type.</summary>
public string? ElementWriterClassName { get; }
/// <summary>Collection type: "List", "Array", or null (unknown — fallback to runtime).</summary>
public string? CollectionKind { get; }
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
public string? ElementFullTypeName { get; }
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null)
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null,
PropertyTypeKind elementKind = PropertyTypeKind.Unknown, bool elementHasGenWriter = false, bool elementIsIId = false,
string? elementWriterClassName = null, string? collectionKind = null, string? elementFullTypeName = null)
{
Name = n;
TypeName = tn;
@ -595,6 +782,12 @@ internal sealed class PropInfo
HasGeneratedWriter = hasGeneratedWriter;
IsIId = isIId;
WriterClassName = writerClassName;
ElementKind = elementKind;
ElementHasGeneratedWriter = elementHasGenWriter;
ElementIsIId = elementIsIId;
ElementWriterClassName = elementWriterClassName;
CollectionKind = collectionKind;
ElementFullTypeName = elementFullTypeName;
// Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase
int flags = 0;
if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit