Improve collection handling in generator; enable all test models

- Refined collection kind mapping: IList<T>/IReadOnlyList<T> now treated as "IndexedCollection" for codegen, distinct from List<T>
- Generated code uses CollectionsMarshal.AsSpan for List<T> iteration, improving performance
- Updated generated read/write logic for collections to match new distinctions
- Added System.Runtime.InteropServices to generated code for span support
- Increased test iteration count in Program.cs for more robust benchmarks
- Enabled source-generated binary serialization for all test models by setting [AcBinarySerializable(true)]
This commit is contained in:
Loretta 2026-02-22 09:09:42 +01:00
parent 5ebcd03e87
commit e6afd21fef
3 changed files with 56 additions and 32 deletions

View File

@ -50,7 +50,7 @@ public static class Program
private static int TestIterations = 1; private static int TestIterations = 1;
#else #else
private static int WarmupIterations = 5000; private static int WarmupIterations = 5000;
private static int TestIterations = 1000; private static int TestIterations = 5000;
//private static int WarmupIterations = 5000; //private static int WarmupIterations = 5000;
//private static int TestIterations = 2000; //private static int TestIterations = 2000;

View File

@ -193,8 +193,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
collKind = origDef switch collKind = origDef switch
{ {
"System.Collections.Generic.List<T>" => "List", "System.Collections.Generic.List<T>" => "List",
"System.Collections.Generic.IList<T>" => "List", // has Count + indexer "System.Collections.Generic.IList<T>" => "IndexedCollection",
"System.Collections.Generic.IReadOnlyList<T>" => "List", // has Count + indexer "System.Collections.Generic.IReadOnlyList<T>" => "IndexedCollection",
"System.Collections.Generic.HashSet<T>" => "Counted", // has Count, no indexer "System.Collections.Generic.HashSet<T>" => "Counted", // has Count, no indexer
"System.Collections.Generic.Queue<T>" => "Counted", "System.Collections.Generic.Queue<T>" => "Counted",
"System.Collections.Generic.ICollection<T>" => "Counted", "System.Collections.Generic.ICollection<T>" => "Counted",
@ -304,6 +304,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine("// <auto-generated/>"); sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable"); sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using System.Runtime.CompilerServices;");
sb.AppendLine("using System.Runtime.InteropServices;");
sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
// ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan // ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan
sb.AppendLine("using AyCode.Core.Serializers;"); sb.AppendLine("using AyCode.Core.Serializers;");
@ -753,6 +754,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
} }
else if (p.CollectionKind == "List") else if (p.CollectionKind == "List")
{
sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});");
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];");
}
else if (p.CollectionKind == "IndexedCollection")
{ {
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)"); sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
@ -812,6 +820,13 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];"); sb.AppendLine($"{i} var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
} }
else if (p.CollectionKind == "List") else if (p.CollectionKind == "List")
{
sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});");
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var se_{p.Name} = span_{p.Name}[si_{p.Name}];");
}
else if (p.CollectionKind == "IndexedCollection")
{ {
sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)"); sb.AppendLine($"{i} for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
@ -1025,14 +1040,23 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})"); sb.AppendLine($"{i} foreach (var elem_{p.Name} in col_{p.Name})");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
} }
else // List, IList<T>, IReadOnlyList<T> — Count + indexer else if (p.CollectionKind == "IndexedCollection")
{ {
sb.AppendLine($"{i} var list_{p.Name} = {a};"); sb.AppendLine($"{i} var col_{p.Name} = {a};");
sb.AppendLine($"{i} context.WriteVarUInt((uint)list_{p.Name}.Count);"); sb.AppendLine($"{i} context.WriteVarUInt((uint)col_{p.Name}.Count);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;"); 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} for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = list_{p.Name}[i_{p.Name}];"); sb.AppendLine($"{i} var elem_{p.Name} = col_{p.Name}[i_{p.Name}];");
}
else // List — CollectionsMarshal.AsSpan for zero-overhead iteration
{
sb.AppendLine($"{i} var span_{p.Name} = CollectionsMarshal.AsSpan({a});");
sb.AppendLine($"{i} context.WriteVarUInt((uint)span_{p.Name}.Length);");
sb.AppendLine($"{i} var nextDepth_{p.Name} = depth + 1;");
sb.AppendLine($"{i} for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var elem_{p.Name} = span_{p.Name}[i_{p.Name}];");
} }
// Per-element write // Per-element write
@ -1542,7 +1566,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan); EmitReadCollectionElement(sb, reader, elemCast, $"ri_{s}", s, i + " ", isArray: true, p.ElementNeedsRefScan);
sb.AppendLine($"{i} }}"); sb.AppendLine($"{i} }}");
} }
else // List, Counted — all use List<T> with Add else // List, IndexedCollection, Counted — all use List<T> with Add
{ {
sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});"); sb.AppendLine($"{i} var col_{s} = new System.Collections.Generic.List<{elemType}>(cnt_{s});");
sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)"); sb.AppendLine($"{i} for (var ri_{s} = 0; ri_{s} < cnt_{s}; ri_{s}++)");
@ -2042,7 +2066,7 @@ internal sealed class PropInfo
public string? ElementWriterClassName { get; } public string? ElementWriterClassName { get; }
/// <summary>Id type name for collection element IId types. Null if not IId.</summary> /// <summary>Id type name for collection element IId types. Null if not IId.</summary>
public string? ElementIdTypeName { get; } public string? ElementIdTypeName { get; }
/// <summary>Collection type: "List", "Array", or null (unknown — fallback to runtime).</summary> /// <summary>Collection type: "List", "Array", "IndexedCollection", "Counted", or null (unknown — fallback to runtime).</summary>
public string? CollectionKind { get; } public string? CollectionKind { get; }
/// <summary>Full element type name for generated code (e.g. "SharedTag").</summary> /// <summary>Full element type name for generated code (e.g. "SharedTag").</summary>
public string? ElementFullTypeName { get; } public string? ElementFullTypeName { get; }

View File

@ -55,7 +55,7 @@ public enum TestUserRole
/// Implements IId&lt;int&gt; for semantic $id/$ref serialization. /// Implements IId&lt;int&gt; for semantic $id/$ref serialization.
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class SharedTag : IId<int> public partial class SharedTag : IId<int>
{ {
@ -80,7 +80,7 @@ public partial class SharedTag : IId<int>
/// Shared category - for hierarchical cross-reference testing. /// Shared category - for hierarchical cross-reference testing.
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class SharedCategory : IId<int> public partial class SharedCategory : IId<int>
{ {
@ -106,7 +106,7 @@ public partial class SharedCategory : IId<int>
/// Shared user reference - appears in many places to test $ref deduplication. /// Shared user reference - appears in many places to test $ref deduplication.
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class SharedUser : IId<int> public partial class SharedUser : IId<int>
{ {
@ -136,7 +136,7 @@ public partial class SharedUser : IId<int>
/// User preferences - non-IId nested object /// User preferences - non-IId nested object
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class UserPreferences public partial class UserPreferences
{ {
@ -162,7 +162,7 @@ public partial class UserPreferences
/// Does NOT implement IId, so uses standard Newtonsoft reference tracking. /// Does NOT implement IId, so uses standard Newtonsoft reference tracking.
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class MetadataInfo public partial class MetadataInfo
{ {
@ -190,7 +190,7 @@ public partial class MetadataInfo
/// Level 1: Main order - root of the hierarchy /// Level 1: Main order - root of the hierarchy
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class TestOrder : IId<int> public partial class TestOrder : IId<int>
{ {
@ -250,7 +250,7 @@ public partial class TestOrder : IId<int>
/// Level 2: Order item with pallets /// Level 2: Order item with pallets
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class TestOrderItem : IId<int> public partial class TestOrderItem : IId<int>
{ {
@ -290,7 +290,7 @@ public partial class TestOrderItem : IId<int>
/// Level 3: Pallet containing measurements /// Level 3: Pallet containing measurements
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class TestPallet : IId<int> public partial class TestPallet : IId<int>
{ {
@ -333,7 +333,7 @@ public partial class TestPallet : IId<int>
/// Level 4: Measurement with multiple points /// Level 4: Measurement with multiple points
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class TestMeasurement : IId<int> public partial class TestMeasurement : IId<int>
{ {
@ -368,7 +368,7 @@ public partial class TestMeasurement : IId<int>
/// Level 5: Deepest level - measurement point /// Level 5: Deepest level - measurement point
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
[MessagePackObject] [MessagePackObject]
public partial class TestMeasurementPoint : IId<int> public partial class TestMeasurementPoint : IId<int>
{ {
@ -402,7 +402,7 @@ public partial class TestMeasurementPoint : IId<int>
/// <summary> /// <summary>
/// Order with Guid Id - for testing Guid-based IId /// Order with Guid Id - for testing Guid-based IId
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class TestGuidOrder : IId<Guid> public class TestGuidOrder : IId<Guid>
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
@ -414,7 +414,7 @@ public class TestGuidOrder : IId<Guid>
/// <summary> /// <summary>
/// Item with Guid Id /// Item with Guid Id
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class TestGuidItem : IId<Guid> public class TestGuidItem : IId<Guid>
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
@ -430,7 +430,7 @@ public class TestGuidItem : IId<Guid>
/// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values /// Simulates NopCommerce GenericAttribute - stores key-value pairs where DateTime values
/// are stored as strings in the database. /// are stored as strings in the database.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class TestGenericAttribute public class TestGenericAttribute
{ {
public int Id { get; set; } public int Id { get; set; }
@ -442,7 +442,7 @@ public class TestGenericAttribute
/// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values. /// DTO with GenericAttributes collection - simulates OrderDto with string-stored DateTime values.
/// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings. /// This reproduces the production bug where Binary serialization was thought to corrupt DateTime strings.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class TestDtoWithGenericAttributes : IId<int> public class TestDtoWithGenericAttributes : IId<int>
{ {
public int Id { get; set; } public int Id { get; set; }
@ -453,7 +453,7 @@ public class TestDtoWithGenericAttributes : IId<int>
/// <summary> /// <summary>
/// Order with nullable collections for null vs empty testing /// Order with nullable collections for null vs empty testing
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class TestOrderWithNullableCollections public class TestOrderWithNullableCollections
{ {
public int Id { get; set; } public int Id { get; set; }
@ -466,7 +466,7 @@ public class TestOrderWithNullableCollections
/// Class with all primitive types for WASM/serialization testing /// Class with all primitive types for WASM/serialization testing
/// </summary> /// </summary>
[MemoryPackable] [MemoryPackable]
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public partial class PrimitiveTestClass public partial class PrimitiveTestClass
{ {
public int IntValue { get; set; } public int IntValue { get; set; }
@ -489,7 +489,7 @@ public partial class PrimitiveTestClass
/// Class with extended primitive types for full serializer coverage. /// Class with extended primitive types for full serializer coverage.
/// Includes DateTimeOffset, TimeSpan, Dictionary, null properties. /// Includes DateTimeOffset, TimeSpan, Dictionary, null properties.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class ExtendedPrimitiveTestClass public class ExtendedPrimitiveTestClass
{ {
public int Id { get; set; } public int Id { get; set; }
@ -519,7 +519,7 @@ public class ExtendedPrimitiveTestClass
/// <summary> /// <summary>
/// Class with array of objects containing null items for WriteNull coverage /// Class with array of objects containing null items for WriteNull coverage
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class ObjectWithNullItems public class ObjectWithNullItems
{ {
public int Id { get; set; } public int Id { get; set; }
@ -534,7 +534,7 @@ public class ObjectWithNullItems
/// "Server-side" DTO with extra properties that the "client" doesn't know about. /// "Server-side" DTO with extra properties that the "client" doesn't know about.
/// Used to test SkipValue functionality when deserializing unknown properties. /// Used to test SkipValue functionality when deserializing unknown properties.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class ServerCustomerDto : IId<int> public class ServerCustomerDto : IId<int>
{ {
public int Id { get; set; } public int Id { get; set; }
@ -567,7 +567,7 @@ public class ServerCustomerDto : IId<int>
/// the deserializer must skip unknown properties correctly /// the deserializer must skip unknown properties correctly
/// while still maintaining string intern table consistency. /// while still maintaining string intern table consistency.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class ClientCustomerDto : IId<int> public class ClientCustomerDto : IId<int>
{ {
public int Id { get; set; } public int Id { get; set; }
@ -581,7 +581,7 @@ public class ClientCustomerDto : IId<int>
/// Server DTO with nested objects that client doesn't know about. /// Server DTO with nested objects that client doesn't know about.
/// Tests skipping complex nested structures. /// Tests skipping complex nested structures.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class ServerOrderWithExtras : IId<int> public class ServerOrderWithExtras : IId<int>
{ {
public int Id { get; set; } public int Id { get; set; }
@ -602,7 +602,7 @@ public class ServerOrderWithExtras : IId<int>
/// <summary> /// <summary>
/// Client version of the order - doesn't have Customer/RelatedCustomers properties. /// Client version of the order - doesn't have Customer/RelatedCustomers properties.
/// </summary> /// </summary>
[AcBinarySerializable(false)] [AcBinarySerializable(true)]
public class ClientOrderSimple : IId<int> public class ClientOrderSimple : IId<int>
{ {
public int Id { get; set; } public int Id { get; set; }