From 7e3fbe7a5270a750e070a501f05d3fc133746b20 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 1 Mar 2026 19:27:38 +0100 Subject: [PATCH] Optimize ref/interning checks; add ScanOnly for benchmarking Refactor serialization context to use precomputed boolean flags (HasRefHandling, HasAllRefHandling, HasStringInterning) for faster reference and string interning checks, replacing repeated enum comparisons. Update source generator to emit code using these flags. Add AcBinarySerializer.ScanOnly for isolated scan benchmarking. Set MaxDepth in test options. Improves performance and maintainability. --- .claude/settings.local.json | 3 +- AyCode.Core.Serializers.Console/Program.cs | 11 ++++- .../AcBinarySourceGenerator.cs | 46 +++++++++---------- .../AcBinarySerializerIIdReferenceTests.cs | 3 +- .../Serializers/AcSerializerContextBase.cs | 11 ++++- ...rySerializer.BinarySerializationContext.cs | 6 ++- .../Binaries/AcBinarySerializer.cs | 24 ++++++++++ 7 files changed, 75 insertions(+), 29 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 350631e..2b08fdb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,7 +36,8 @@ "Bash(del \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\IBinaryOutput.cs\")", "Bash(sort:*)", "WebFetch(domain:neuecc.medium.com)", - "WebFetch(domain:raw.githubusercontent.com)" + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(xargs cat)" ] } } diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index a645910..3ec0eca 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -289,7 +289,16 @@ public static class Program } [MethodImpl(MethodImplOptions.NoInlining)] - public void Serialize() => AcBinarySerializer.Serialize(_order, _options); + public void Serialize() + { + AcBinarySerializer.Serialize(_order, _options); + + //if (_options.ReferenceHandling != ReferenceHandlingMode.None || _options.UseStringInterning != StringInterningMode.None) + //{ + // AcBinarySerializer.ScanOnly(_order, _options); + //} + //else AcBinarySerializer.Serialize(_order, _options); + } [MethodImpl(MethodImplOptions.NoInlining)] public void Deserialize() => AcBinaryDeserializer.Deserialize(_serialized, _options); diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 46a4e92..1538ff0 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -498,7 +498,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using System.Runtime.InteropServices;"); sb.AppendLine("using AyCode.Core.Serializers.Binaries;"); - // ReferenceHandlingMode is needed for ScanObject self ref tracking and direct object write/scan + // IGeneratedBinaryWriter and other serializer types sb.AppendLine("using AyCode.Core.Serializers;"); sb.AppendLine(); if (!string.IsNullOrEmpty(ci.Namespace)) @@ -566,11 +566,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (!ci.NeedsIdScan) { if (ci.NeedsAllRefScan && ci.NeedsInternScan) - sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All && !context.UseStringInterning) return;"); + sb.AppendLine(" if (!context.HasAllRefHandling && !context.HasStringInterning) return;"); else if (ci.NeedsAllRefScan) - sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All) return;"); + sb.AppendLine(" if (!context.HasAllRefHandling) return;"); else if (ci.NeedsInternScan) - sb.AppendLine(" if (!context.UseStringInterning) return;"); + sb.AppendLine(" if (!context.HasStringInterning) return;"); } // Null/depth guard — matches runtime ScanValue entry @@ -590,7 +590,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator _ => "TryTrackInt32" }; sb.AppendLine(); - sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.None)"); + sb.AppendLine(" if (context.HasRefHandling)"); sb.AppendLine(" {"); sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); @@ -607,7 +607,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { // Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode sb.AppendLine(); - sb.AppendLine(" if (context.ReferenceHandling == ReferenceHandlingMode.All)"); + sb.AppendLine(" if (context.HasAllRefHandling)"); sb.AppendLine(" {"); sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); @@ -914,11 +914,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (!p.ChildNeedsIdScan) { if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan) - guard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; + guard = "context.HasAllRefHandling || context.HasStringInterning"; else if (p.ChildNeedsAllRefScan) - guard = "context.ReferenceHandling == ReferenceHandlingMode.All"; + guard = "context.HasAllRefHandling"; else if (p.ChildNeedsInternScan) - guard = "context.UseStringInterning"; + guard = "context.HasStringInterning"; } if (guard != null) @@ -1018,11 +1018,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (!p.ElementNeedsIdScan) { if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan) - elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; + elemGuard = "context.HasAllRefHandling || context.HasStringInterning"; else if (p.ElementNeedsAllRefScan) - elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All"; + elemGuard = "context.HasAllRefHandling"; else if (p.ElementNeedsInternScan) - elemGuard = "context.UseStringInterning"; + elemGuard = "context.HasStringInterning"; } // Guard entire collection scan with runtime check when no IId in element subtree @@ -1107,11 +1107,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (hasComplexValues && p.DictValueNeedsScan && !p.DictValueNeedsIdScan) { if (p.DictValueNeedsAllRefScan && p.DictValueNeedsInternScan) - complexGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; + complexGuard = "context.HasAllRefHandling || context.HasStringInterning"; else if (p.DictValueNeedsAllRefScan) - complexGuard = "context.ReferenceHandling == ReferenceHandlingMode.All"; + complexGuard = "context.HasAllRefHandling"; else if (p.DictValueNeedsInternScan) - complexGuard = "context.UseStringInterning"; + complexGuard = "context.HasStringInterning"; } // For string-only scan (no complex values), use simple interning loop @@ -1257,8 +1257,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef var refGuard = p.IsIId - ? "context.ReferenceHandling != ReferenceHandlingMode.None" - : "context.ReferenceHandling == ReferenceHandlingMode.All"; + ? "context.HasRefHandling" + : "context.HasAllRefHandling"; sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); @@ -1284,8 +1284,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator // Full path: ref tracking + metadata sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); var refGuard = p.IsIId - ? "context.ReferenceHandling != ReferenceHandlingMode.None" - : "context.ReferenceHandling == ReferenceHandlingMode.All"; + ? "context.HasRefHandling" + : "context.HasAllRefHandling"; sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); @@ -1436,8 +1436,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator { // Inline ref tracking var elemRefGuard = p.ElementIsIId - ? "context.ReferenceHandling != ReferenceHandlingMode.None" - : "context.ReferenceHandling == ReferenceHandlingMode.All"; + ? "context.HasRefHandling" + : "context.HasAllRefHandling"; if (!p.ElementEnableMetadata) { @@ -1700,8 +1700,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator else { var dvRefGuard = p.DictValueIsIId - ? "context.ReferenceHandling != ReferenceHandlingMode.None" - : "context.ReferenceHandling == ReferenceHandlingMode.All"; + ? "context.HasRefHandling" + : "context.HasAllRefHandling"; if (!p.DictValueEnableMetadata) { diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs index 490287c..33a33cb 100644 --- a/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs +++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerIIdReferenceTests.cs @@ -127,7 +127,8 @@ public class AcBinarySerializerIIdReferenceTests { ReferenceHandling = mode, UseGeneratedCode = useSgen, - UseMetadata = useMeta + UseMetadata = useMeta, + MaxDepth = 10 }; Console.WriteLine($"\n========== ReferenceHandling: {options.ReferenceHandling}, UseSgen: {options.UseGeneratedCode}, UseMeta: {options.UseMetadata} =========="); diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index 71d1528..8a5c84d 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -31,9 +31,17 @@ public abstract class AcSerializerContextBase private bool _hasRefHandling; /// - /// Pre-computed: ReferenceHandling == IId (not All). When true, only IId types are tracked. + /// Pre-computed: ReferenceHandling == OnlyId (not All). When true, only IId types are tracked. /// private bool _hasIdHandling; + + /// + /// Pre-computed: ReferenceHandling == All. When true, all reference types are tracked. + /// + private bool _hasAllRefHandling; + + internal bool HasRefHandling => _hasRefHandling; + internal bool HasAllRefHandling => _hasAllRefHandling; public bool ThrowOnCircularReference => Options.ThrowOnCircularReference; /// /// Global shared cache for metadata (thread-safe, shared across all contexts). @@ -165,6 +173,7 @@ public abstract class AcSerializerContextBase Options = options; _hasRefHandling = options.ReferenceHandling != ReferenceHandlingMode.None; _hasIdHandling = options.ReferenceHandling == ReferenceHandlingMode.OnlyId; + _hasAllRefHandling = options.ReferenceHandling == ReferenceHandlingMode.All; } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index ee8c387..576ebea 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -212,7 +212,8 @@ public static partial class AcBinarySerializer #endif // These properties delegate to Options for convenience - internal bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None; + internal bool HasStringInterning { get; private set; } + internal bool UseStringInterning => HasStringInterning; /// /// Pre-computed 1 << (int)Options.UseStringInterning. @@ -230,7 +231,7 @@ public static partial class AcBinarySerializer /// /// True if we have interning/ref tracking (cache count needed in header). /// - public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; + public bool HasCaching => HasStringInterning || HasRefHandling; public bool UseMetadata => Options.UseMetadata; public bool UseGeneratedCode => Options.UseGeneratedCode; @@ -279,6 +280,7 @@ public static partial class AcBinarySerializer base.Reset(options); HasPropertyFilter = Options.PropertyFilter != null; InternBit = 1 << (int)Options.UseStringInterning; + HasStringInterning = Options.UseStringInterning != StringInterningMode.None; //FastWire = Options.WireMode == WireMode.Fast; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index d6ab191..c34167d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -342,6 +342,30 @@ public static partial class AcBinarySerializer } } + /// + /// Runs only the scan pass (ScanForDuplicates) without writing. + /// For benchmarking scan pass overhead in isolation. + /// + internal static void ScanOnly(T value, AcBinarySerializerOptions options) + { + if (value == null) return; + var runtimeType = value.GetType(); + var context = BinarySerializationContextPool.Get(options); + if (!context.OutputInitialized) + { + context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity); + context.OutputInitialized = true; + } + try + { + ScanForDuplicates(value, runtimeType, context); + } + finally + { + BinarySerializationContextPool.Return(context); + } + } + /// /// Serialize object to an IBufferWriter for zero-copy scenarios. /// Uses BufferWriterBinaryOutput — writes directly to the caller's buffer.