Optimize scan codegen with compile-time scan analysis

Added compile-time scan requirement flags to SerializableClassInfo and PropInfo, and implemented recursive analysis to determine if scan work is needed for reference tracking and string interning. Updated code generation to emit scan code only when necessary, with runtime guards based on compile-time analysis. Changed AcBinarySerializerOptions.UseMetadata default to false. Increased JIT wait in Program.cs for more reliable benchmarking. These changes reduce unnecessary scan calls and improve performance.
This commit is contained in:
Loretta 2026-02-20 15:57:20 +01:00
parent 3e935cad2f
commit cb2ee24a4c
3 changed files with 246 additions and 14 deletions

View File

@ -175,7 +175,7 @@ public static class Program
}
// Wait for tiered JIT background compilation to complete
Thread.Sleep(2000);
Thread.Sleep(3000);
// Run benchmarks
System.Console.WriteLine($"Running benchmarks ({TestIterations} iterations)...\n");

View File

@ -114,6 +114,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
bool hasGenWriter = false;
bool propTypeIsIId = false;
bool propEnableMetadata = true;
bool childNeedsIdScan = true;
bool childNeedsAllRefScan = true;
bool childNeedsInternScan = true;
string? writerClassName = null;
string? propIdTypeName = null;
int childTypeNameHash = 0;
@ -133,6 +136,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{
// Read child type's EnableMetadataFeature
propEnableMetadata = ReadEnableMetadata(resolvedType);
var childScanFlags = ComputeNeedsScan(resolvedType);
childNeedsIdScan = childScanFlags.needsIdScan;
childNeedsAllRefScan = childScanFlags.needsAllRefScan;
childNeedsInternScan = childScanFlags.needsInternScan;
var iidIface = resolvedType.AllInterfaces.FirstOrDefault(i =>
i.IsGenericType &&
i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
@ -160,6 +167,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
bool elemHasGenWriter = false;
bool elemIsIId = false;
bool elemEnableMetadata = true;
bool elemNeedsIdScan = true;
bool elemNeedsAllRefScan = true;
bool elemNeedsInternScan = true;
string? elemWriterClassName = null;
string? elemIdTypeName = null;
string? collKind = null;
@ -206,6 +216,10 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{
// Read element type's EnableMetadataFeature
elemEnableMetadata = ReadEnableMetadata(resolvedElem);
var elemScanFlags = ComputeNeedsScan(resolvedElem);
elemNeedsIdScan = elemScanFlags.needsIdScan;
elemNeedsAllRefScan = elemScanFlags.needsAllRefScan;
elemNeedsInternScan = elemScanFlags.needsInternScan;
var elemIidIface = resolvedElem.AllInterfaces.FirstOrDefault(ifc =>
ifc.IsGenericType &&
ifc.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
@ -238,7 +252,9 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
elemKind, elemHasGenWriter, elemIsIId, elemWriterClassName, elemIdTypeName, collKind, elemFullTypeName,
childTypeNameHash, childPropertyHashes,
elementTypeNameHash, elementPropertyHashes,
propEnableMetadata, elemEnableMetadata));
propEnableMetadata, elemEnableMetadata,
childNeedsIdScan, childNeedsAllRefScan, childNeedsInternScan,
elemNeedsIdScan, elemNeedsAllRefScan, elemNeedsInternScan));
}
}
@ -263,7 +279,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
var className = BuildFlatName(typeSymbol);
var typeNameHash = ComputeFnvHash(typeSymbol.Name);
var propertyNameHashes = properties.Select(prop => ComputeFnvHash(prop.Name)).ToArray();
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata);
var selfScanFlags = ComputeNeedsScan(typeSymbol);
return new SerializableClassInfo(namespaceName, className, typeSymbol.ToDisplayString(), properties, isIId, idTypeName, enableRefHandling, typeNameHash, propertyNameHashes, enableMetadata, selfScanFlags.needsIdScan, selfScanFlags.needsAllRefScan, selfScanFlags.needsInternScan);
}
private static void Execute(ImmutableArray<SerializableClassInfo?> classes, SourceProductionContext context)
@ -343,6 +360,25 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(" public void ScanObject<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth) where TOutput : struct, IBinaryOutputBase");
sb.AppendLine(" {");
// Compile-time proven: no scan work needed for this type
if (!ci.NeedsScan)
{
sb.AppendLine(" // NeedsScan=false: no ref tracking, no string interning, no scannable children");
sb.AppendLine(" }");
return;
}
// Early return: skip scan when no active runtime feature matches this type's needs
if (!ci.NeedsIdScan)
{
if (ci.NeedsAllRefScan && ci.NeedsInternScan)
sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All && !context.UseStringInterning) return;");
else if (ci.NeedsAllRefScan)
sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All) return;");
else if (ci.NeedsInternScan)
sb.AppendLine(" if (!context.UseStringInterning) return;");
}
// Null/depth guard — matches runtime ScanValue entry
sb.AppendLine(" if (value == null || depth > context.MaxDepth) return;");
sb.AppendLine();
@ -634,14 +670,41 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
/// </summary>
private static void EmitScanComplexSGen(StringBuilder sb, PropInfo p, string a, string i)
{
// Compile-time proven: child scan is no-op — skip entirely
if (!p.ChildNeedsScan) return;
var writer = p.WriterClassName;
var childVar = $"sc_{p.Name}";
// Null check only — ScanObject handles depth + ref tracking internally
// 3-axis guard: IId → always call, AllRef → guard All mode, Intern → guard UseStringInterning
string? guard = null;
if (!p.ChildNeedsIdScan)
{
if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan)
guard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning";
else if (p.ChildNeedsAllRefScan)
guard = "context.ReferenceHandling == ReferenceHandlingMode.All";
else if (p.ChildNeedsInternScan)
guard = "context.UseStringInterning";
}
if (guard != null)
{
sb.AppendLine($"{i}if ({guard})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var {childVar} = {a};");
sb.AppendLine($"{i} if ({childVar} != null)");
sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);");
sb.AppendLine($"{i}}}");
}
else
{
// IId in subtree — always call (active in OnlyId + All)
sb.AppendLine($"{i}var {childVar} = {a};");
sb.AppendLine($"{i}if ({childVar} != null)");
sb.AppendLine($"{i} {writer}.Instance.ScanObject({childVar}, context, depth + 1);");
}
}
/// <summary>
/// Emits scan pass code for a Complex property without SGen writer (runtime fallback).
@ -701,11 +764,31 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// Complex element collection with SGen writer
if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null)
{
// Compile-time proven: element scan is no-op — skip entirely
if (!p.ElementNeedsScan) return;
var writer = p.ElementWriterClassName;
sb.AppendLine($"{i}var scol_{p.Name} = {a};");
sb.AppendLine($"{i}if (scol_{p.Name} != null)");
// 3-axis guard: IId → always scan, AllRef → guard All mode, Intern → guard UseStringInterning
string? elemGuard = null;
if (!p.ElementNeedsIdScan)
{
if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan)
elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning";
else if (p.ElementNeedsAllRefScan)
elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All";
else if (p.ElementNeedsInternScan)
elemGuard = "context.UseStringInterning";
}
// Guard entire collection scan with runtime check when no IId in element subtree
if (elemGuard != null)
sb.AppendLine($"{i}if ({elemGuard})");
sb.AppendLine($"{i}{{");
sb.AppendLine($"{i} var scol_{p.Name} = {a};");
sb.AppendLine($"{i} if (scol_{p.Name} != null)");
sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} var snd_{p.Name} = depth + 1;");
if (p.CollectionKind == "Array")
@ -731,6 +814,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine($"{i} if ({e} == null) continue;");
sb.AppendLine($"{i} {writer}.Instance.ScanObject({e}, context, snd_{p.Name});");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i} }}");
sb.AppendLine($"{i}}}");
return;
}
@ -1168,6 +1252,116 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
return true;
}
/// <summary>
/// Computes whether a type needs scan pass work, split into ref tracking and string interning.
/// Uses a per-call HashSet to guard against circular references (no static cache —
/// static state is unsafe in incremental generators as it persists across builds).
/// Returns (needsRefScan, needsInternScan) — these are independent axes.
/// </summary>
private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScan(ITypeSymbol type)
{
return ComputeNeedsScanCore(type, new HashSet<string>());
}
private static (bool needsIdScan, bool needsAllRefScan, bool needsInternScan) ComputeNeedsScanCore(ITypeSymbol type, HashSet<string> visiting)
{
// Circular reference guard: if already visiting this type, assume true (safe fallback)
var key = type.ToDisplayString();
if (!visiting.Add(key))
return (true, true, true);
// Read [AcBinarySerializable] flags
var attr = type.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == AttributeName);
bool enableIdTracking = true, enableRefHandling = true, enableInternString = true;
if (attr != null)
{
if (attr.ConstructorArguments.Length == 1)
{
var all = (bool)attr.ConstructorArguments[0].Value!;
enableIdTracking = enableRefHandling = enableInternString = all;
}
else if (attr.ConstructorArguments.Length == 4)
{
enableIdTracking = (bool)attr.ConstructorArguments[1].Value!;
enableRefHandling = (bool)attr.ConstructorArguments[2].Value!;
enableInternString = (bool)attr.ConstructorArguments[3].Value!;
}
}
// IId tracking: active in OnlyId + All modes
var isIId = enableIdTracking && type.AllInterfaces.Any(i =>
i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "AyCode.Core.Interfaces.IId<T>");
var needsIdScan = isIId;
// Non-IId ref tracking: active only in All mode
var needsAllRefScan = !isIId && enableRefHandling;
var needsInternScan = false;
// Check properties for string interning or complex children
foreach (var member in type.GetMembers())
{
if (member is not IPropertySymbol p ||
p.DeclaredAccessibility != Accessibility.Public ||
p.GetMethod == null || p.SetMethod == null ||
p.IsIndexer || p.IsStatic)
continue;
var hasIgnore = p.GetAttributes().Any(a =>
{
var name = a.AttributeClass?.Name ?? "";
return name == "JsonIgnoreAttribute" || name == "IgnoreMemberAttribute" || name == "BsonIgnoreAttribute";
});
if (hasIgnore) continue;
// Early exit: if all flags are already true, no need to check more properties
if (needsIdScan && needsAllRefScan && needsInternScan) break;
var kind = GetKind(p.Type);
// String with interning?
if (enableInternString && kind == PropertyTypeKind.String)
{
var internAttr = p.GetAttributes().FirstOrDefault(a =>
a.AttributeClass?.ToDisplayString() == "AyCode.Core.Serializers.Binaries.AcStringInternAttribute");
if (internAttr == null || (internAttr.ConstructorArguments.Length == 1 && (bool)internAttr.ConstructorArguments[0].Value!))
needsInternScan = true;
}
// Complex child → recurse
if (kind == PropertyTypeKind.Complex)
{
var resolved = p.Type is INamedTypeSymbol nt ? nt.OriginalDefinition : p.Type;
var childFlags = ComputeNeedsScanCore(resolved, visiting);
needsIdScan |= childFlags.needsIdScan;
needsAllRefScan |= childFlags.needsAllRefScan;
needsInternScan |= childFlags.needsInternScan;
}
// Collection → check element type
if (kind == PropertyTypeKind.Collection)
{
var elemType = GetCollectionElementType(p.Type);
if (elemType != null)
{
var elemKind = GetKind(elemType);
if (enableInternString && elemKind == PropertyTypeKind.String)
needsInternScan = true;
if (elemKind == PropertyTypeKind.Complex)
{
var resolvedElem = elemType is INamedTypeSymbol ne ? ne.OriginalDefinition : elemType;
var elemFlags = ComputeNeedsScanCore(resolvedElem, visiting);
needsIdScan |= elemFlags.needsIdScan;
needsAllRefScan |= elemFlags.needsAllRefScan;
needsInternScan |= elemFlags.needsInternScan;
}
}
}
}
return (needsIdScan, needsAllRefScan, needsInternScan);
}
#region FNV-1a Hash (compile-time)
private static int ComputeFnvHash(string value)
@ -1315,8 +1509,18 @@ internal sealed class SerializableClassInfo
public int[] PropertyNameHashes { get; }
/// <summary>When false, skip inline metadata and use markerless property write for this type.</summary>
public bool EnableMetadata { get; }
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata)
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; }
/// <summary>When true, type subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool NeedsIdScan { get; }
/// <summary>When true, type subtree has non-IId ref tracking (active only in All mode).</summary>
public bool NeedsAllRefScan { get; }
/// <summary>When true, type subtree needs string interning scan.</summary>
public bool NeedsInternScan { get; }
/// <summary>Derived: NeedsIdScan || NeedsAllRefScan.</summary>
public bool NeedsRefScan => NeedsIdScan || NeedsAllRefScan;
/// <summary>Derived: any scan axis active.</summary>
public bool NeedsScan => NeedsIdScan || NeedsAllRefScan || NeedsInternScan;
public SerializableClassInfo(string ns, string cn, string ftn, List<PropInfo> p, bool isIId, string? idTypeName, bool enableRefHandling, int typeNameHash, int[] propertyNameHashes, bool enableMetadata, bool needsIdScan, bool needsAllRefScan, bool needsInternScan)
{ Namespace = ns; ClassName = cn; FullTypeName = ftn; Properties = p; IsIId = isIId; IdTypeName = idTypeName; EnableRefHandling = enableRefHandling; TypeNameHash = typeNameHash; PropertyNameHashes = propertyNameHashes; EnableMetadata = enableMetadata; NeedsIdScan = needsIdScan; NeedsAllRefScan = needsAllRefScan; NeedsInternScan = needsInternScan; }
}
internal sealed class PropInfo
@ -1375,6 +1579,26 @@ internal sealed class PropInfo
public bool ChildEnableMetadata { get; }
/// <summary>When false, collection element type skips inline metadata in generated code.</summary>
public bool ElementEnableMetadata { get; }
/// <summary>When true, child subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool ChildNeedsIdScan { get; }
/// <summary>When true, child subtree has non-IId ref tracking (active only in All mode).</summary>
public bool ChildNeedsAllRefScan { get; }
/// <summary>When true, child subtree needs string interning scan.</summary>
public bool ChildNeedsInternScan { get; }
/// <summary>Derived: ChildNeedsIdScan || ChildNeedsAllRefScan.</summary>
public bool ChildNeedsRefScan => ChildNeedsIdScan || ChildNeedsAllRefScan;
/// <summary>Derived: any child scan axis active.</summary>
public bool ChildNeedsScan => ChildNeedsIdScan || ChildNeedsAllRefScan || ChildNeedsInternScan;
/// <summary>When true, element subtree has IId types needing scan (active in OnlyId + All).</summary>
public bool ElementNeedsIdScan { get; }
/// <summary>When true, element subtree has non-IId ref tracking (active only in All mode).</summary>
public bool ElementNeedsAllRefScan { get; }
/// <summary>When true, element subtree needs string interning scan.</summary>
public bool ElementNeedsInternScan { get; }
/// <summary>Derived: ElementNeedsIdScan || ElementNeedsAllRefScan.</summary>
public bool ElementNeedsRefScan => ElementNeedsIdScan || ElementNeedsAllRefScan;
/// <summary>Derived: any element scan axis active.</summary>
public bool ElementNeedsScan => ElementNeedsIdScan || ElementNeedsAllRefScan || ElementNeedsInternScan;
public PropInfo(string n, string tn, string tnForTypeof, PropertyTypeKind tk, bool nullable,
bool? stringInternAttr = null, bool hasGeneratedWriter = false, bool isIId = false, string? writerClassName = null, string? idTypeName = null,
@ -1382,7 +1606,9 @@ internal sealed class PropInfo
string? elementWriterClassName = null, string? elementIdTypeName = null, string? collectionKind = null, string? elementFullTypeName = null,
int childTypeNameHash = 0, int[]? childPropertyHashes = null,
int elementTypeNameHash = 0, int[]? elementPropertyHashes = null,
bool childEnableMetadata = true, bool elementEnableMetadata = true)
bool childEnableMetadata = true, bool elementEnableMetadata = true,
bool childNeedsIdScan = true, bool childNeedsAllRefScan = true, bool childNeedsInternScan = true,
bool elementNeedsIdScan = true, bool elementNeedsAllRefScan = true, bool elementNeedsInternScan = true)
{
Name = n;
TypeName = tn;
@ -1406,6 +1632,12 @@ internal sealed class PropInfo
ElementPropertyHashes = elementPropertyHashes;
ChildEnableMetadata = childEnableMetadata;
ElementEnableMetadata = elementEnableMetadata;
ChildNeedsIdScan = childNeedsIdScan;
ChildNeedsAllRefScan = childNeedsAllRefScan;
ChildNeedsInternScan = childNeedsInternScan;
ElementNeedsIdScan = elementNeedsIdScan;
ElementNeedsAllRefScan = elementNeedsAllRefScan;
ElementNeedsInternScan = elementNeedsInternScan;
// Mirror runtime _interningFlags computation from BinaryPropertyAccessorBase
int flags = 0;
if (stringInternAttr == true) flags |= (1 << 1); // Attribute bit

View File

@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// allowing the deserializer to match properties by name between different types.
/// Default: false (no overhead)
/// </summary>
public bool UseMetadata { get; set; } = true;
public bool UseMetadata { get; set; } = false;
public bool UseGeneratedCode { get; set; } = true;