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:
parent
9973b6be12
commit
7977feb36a
|
|
@ -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<T> / 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<T>, T[], IList<T>, IEnumerable<T>.
|
||||
/// 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<T>.</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
|
||||
|
|
|
|||
Loading…
Reference in New Issue