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.
This commit is contained in:
Loretta 2026-03-01 19:27:38 +01:00
parent 97ece85ee1
commit 7e3fbe7a52
7 changed files with 75 additions and 29 deletions

View File

@ -36,7 +36,8 @@
"Bash(del \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\IBinaryOutput.cs\")", "Bash(del \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Binaries\\\\IBinaryOutput.cs\")",
"Bash(sort:*)", "Bash(sort:*)",
"WebFetch(domain:neuecc.medium.com)", "WebFetch(domain:neuecc.medium.com)",
"WebFetch(domain:raw.githubusercontent.com)" "WebFetch(domain:raw.githubusercontent.com)",
"Bash(xargs cat)"
] ]
} }
} }

View File

@ -289,7 +289,16 @@ public static class Program
} }
[MethodImpl(MethodImplOptions.NoInlining)] [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)] [MethodImpl(MethodImplOptions.NoInlining)]
public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options); public void Deserialize() => AcBinaryDeserializer.Deserialize<TestOrder>(_serialized, _options);

View File

@ -498,7 +498,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine("using System.Runtime.CompilerServices;"); sb.AppendLine("using System.Runtime.CompilerServices;");
sb.AppendLine("using System.Runtime.InteropServices;"); 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 // IGeneratedBinaryWriter and other serializer types
sb.AppendLine("using AyCode.Core.Serializers;"); sb.AppendLine("using AyCode.Core.Serializers;");
sb.AppendLine(); sb.AppendLine();
if (!string.IsNullOrEmpty(ci.Namespace)) if (!string.IsNullOrEmpty(ci.Namespace))
@ -566,11 +566,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (!ci.NeedsIdScan) if (!ci.NeedsIdScan)
{ {
if (ci.NeedsAllRefScan && ci.NeedsInternScan) 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) else if (ci.NeedsAllRefScan)
sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.All) return;"); sb.AppendLine(" if (!context.HasAllRefHandling) return;");
else if (ci.NeedsInternScan) else if (ci.NeedsInternScan)
sb.AppendLine(" if (!context.UseStringInterning) return;"); sb.AppendLine(" if (!context.HasStringInterning) return;");
} }
// Null/depth guard — matches runtime ScanValue entry // Null/depth guard — matches runtime ScanValue entry
@ -590,7 +590,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
_ => "TryTrackInt32" _ => "TryTrackInt32"
}; };
sb.AppendLine(); sb.AppendLine();
sb.AppendLine(" if (context.ReferenceHandling != ReferenceHandlingMode.None)"); sb.AppendLine(" if (context.HasRefHandling)");
sb.AppendLine(" {"); sb.AppendLine(" {");
sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));");
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
@ -607,7 +607,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{ {
// Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode // Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode
sb.AppendLine(); sb.AppendLine();
sb.AppendLine(" if (context.ReferenceHandling == ReferenceHandlingMode.All)"); sb.AppendLine(" if (context.HasAllRefHandling)");
sb.AppendLine(" {"); sb.AppendLine(" {");
sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));");
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
@ -914,11 +914,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (!p.ChildNeedsIdScan) if (!p.ChildNeedsIdScan)
{ {
if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan) if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan)
guard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; guard = "context.HasAllRefHandling || context.HasStringInterning";
else if (p.ChildNeedsAllRefScan) else if (p.ChildNeedsAllRefScan)
guard = "context.ReferenceHandling == ReferenceHandlingMode.All"; guard = "context.HasAllRefHandling";
else if (p.ChildNeedsInternScan) else if (p.ChildNeedsInternScan)
guard = "context.UseStringInterning"; guard = "context.HasStringInterning";
} }
if (guard != null) if (guard != null)
@ -1018,11 +1018,11 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
if (!p.ElementNeedsIdScan) if (!p.ElementNeedsIdScan)
{ {
if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan) if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan)
elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; elemGuard = "context.HasAllRefHandling || context.HasStringInterning";
else if (p.ElementNeedsAllRefScan) else if (p.ElementNeedsAllRefScan)
elemGuard = "context.ReferenceHandling == ReferenceHandlingMode.All"; elemGuard = "context.HasAllRefHandling";
else if (p.ElementNeedsInternScan) else if (p.ElementNeedsInternScan)
elemGuard = "context.UseStringInterning"; elemGuard = "context.HasStringInterning";
} }
// Guard entire collection scan with runtime check when no IId in element subtree // 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 (hasComplexValues && p.DictValueNeedsScan && !p.DictValueNeedsIdScan)
{ {
if (p.DictValueNeedsAllRefScan && p.DictValueNeedsInternScan) if (p.DictValueNeedsAllRefScan && p.DictValueNeedsInternScan)
complexGuard = "context.ReferenceHandling == ReferenceHandlingMode.All || context.UseStringInterning"; complexGuard = "context.HasAllRefHandling || context.HasStringInterning";
else if (p.DictValueNeedsAllRefScan) else if (p.DictValueNeedsAllRefScan)
complexGuard = "context.ReferenceHandling == ReferenceHandlingMode.All"; complexGuard = "context.HasAllRefHandling";
else if (p.DictValueNeedsInternScan) else if (p.DictValueNeedsInternScan)
complexGuard = "context.UseStringInterning"; complexGuard = "context.HasStringInterning";
} }
// For string-only scan (no complex values), use simple interning loop // 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 // Ref tracking possible, no metadata — Object or ObjectRefFirst/ObjectRef
var refGuard = p.IsIId var refGuard = p.IsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None" ? "context.HasRefHandling"
: "context.ReferenceHandling == ReferenceHandlingMode.All"; : "context.HasAllRefHandling";
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
@ -1284,8 +1284,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
// Full path: ref tracking + metadata // Full path: ref tracking + metadata
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));");
var refGuard = p.IsIId var refGuard = p.IsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None" ? "context.HasRefHandling"
: "context.ReferenceHandling == ReferenceHandlingMode.All"; : "context.HasAllRefHandling";
sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))"); sb.AppendLine($"{i} if ({refGuard} && context.TryConsumeWritePlanEntry(out var pe_{p.Name}))");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} if (!pe_{p.Name}.IsFirst)");
@ -1436,8 +1436,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
{ {
// Inline ref tracking // Inline ref tracking
var elemRefGuard = p.ElementIsIId var elemRefGuard = p.ElementIsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None" ? "context.HasRefHandling"
: "context.ReferenceHandling == ReferenceHandlingMode.All"; : "context.HasAllRefHandling";
if (!p.ElementEnableMetadata) if (!p.ElementEnableMetadata)
{ {
@ -1700,8 +1700,8 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
var dvRefGuard = p.DictValueIsIId var dvRefGuard = p.DictValueIsIId
? "context.ReferenceHandling != ReferenceHandlingMode.None" ? "context.HasRefHandling"
: "context.ReferenceHandling == ReferenceHandlingMode.All"; : "context.HasAllRefHandling";
if (!p.DictValueEnableMetadata) if (!p.DictValueEnableMetadata)
{ {

View File

@ -127,7 +127,8 @@ public class AcBinarySerializerIIdReferenceTests
{ {
ReferenceHandling = mode, ReferenceHandling = mode,
UseGeneratedCode = useSgen, UseGeneratedCode = useSgen,
UseMetadata = useMeta UseMetadata = useMeta,
MaxDepth = 10
}; };
Console.WriteLine($"\n========== ReferenceHandling: {options.ReferenceHandling}, UseSgen: {options.UseGeneratedCode}, UseMeta: {options.UseMetadata} =========="); Console.WriteLine($"\n========== ReferenceHandling: {options.ReferenceHandling}, UseSgen: {options.UseGeneratedCode}, UseMeta: {options.UseMetadata} ==========");

View File

@ -31,9 +31,17 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
private bool _hasRefHandling; private bool _hasRefHandling;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private bool _hasIdHandling; private bool _hasIdHandling;
/// <summary>
/// Pre-computed: ReferenceHandling == All. When true, all reference types are tracked.
/// </summary>
private bool _hasAllRefHandling;
internal bool HasRefHandling => _hasRefHandling;
internal bool HasAllRefHandling => _hasAllRefHandling;
public bool ThrowOnCircularReference => Options.ThrowOnCircularReference; public bool ThrowOnCircularReference => Options.ThrowOnCircularReference;
/// <summary> /// <summary>
/// Global shared cache for metadata (thread-safe, shared across all contexts). /// Global shared cache for metadata (thread-safe, shared across all contexts).
@ -165,6 +173,7 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
Options = options; Options = options;
_hasRefHandling = options.ReferenceHandling != ReferenceHandlingMode.None; _hasRefHandling = options.ReferenceHandling != ReferenceHandlingMode.None;
_hasIdHandling = options.ReferenceHandling == ReferenceHandlingMode.OnlyId; _hasIdHandling = options.ReferenceHandling == ReferenceHandlingMode.OnlyId;
_hasAllRefHandling = options.ReferenceHandling == ReferenceHandlingMode.All;
} }
/// <summary> /// <summary>

View File

@ -212,7 +212,8 @@ public static partial class AcBinarySerializer
#endif #endif
// These properties delegate to Options for convenience // These properties delegate to Options for convenience
internal bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None; internal bool HasStringInterning { get; private set; }
internal bool UseStringInterning => HasStringInterning;
/// <summary> /// <summary>
/// Pre-computed <c>1 &lt;&lt; (int)Options.UseStringInterning</c>. /// Pre-computed <c>1 &lt;&lt; (int)Options.UseStringInterning</c>.
@ -230,7 +231,7 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// True if we have interning/ref tracking (cache count needed in header). /// True if we have interning/ref tracking (cache count needed in header).
/// </summary> /// </summary>
public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; public bool HasCaching => HasStringInterning || HasRefHandling;
public bool UseMetadata => Options.UseMetadata; public bool UseMetadata => Options.UseMetadata;
public bool UseGeneratedCode => Options.UseGeneratedCode; public bool UseGeneratedCode => Options.UseGeneratedCode;
@ -279,6 +280,7 @@ public static partial class AcBinarySerializer
base.Reset(options); base.Reset(options);
HasPropertyFilter = Options.PropertyFilter != null; HasPropertyFilter = Options.PropertyFilter != null;
InternBit = 1 << (int)Options.UseStringInterning; InternBit = 1 << (int)Options.UseStringInterning;
HasStringInterning = Options.UseStringInterning != StringInterningMode.None;
//FastWire = Options.WireMode == WireMode.Fast; //FastWire = Options.WireMode == WireMode.Fast;
} }

View File

@ -342,6 +342,30 @@ public static partial class AcBinarySerializer
} }
} }
/// <summary>
/// Runs only the scan pass (ScanForDuplicates) without writing.
/// For benchmarking scan pass overhead in isolation.
/// </summary>
internal static void ScanOnly<T>(T value, AcBinarySerializerOptions options)
{
if (value == null) return;
var runtimeType = value.GetType();
var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
if (!context.OutputInitialized)
{
context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
context.OutputInitialized = true;
}
try
{
ScanForDuplicates(value, runtimeType, context);
}
finally
{
BinarySerializationContextPool<ArrayBinaryOutput>.Return(context);
}
}
/// <summary> /// <summary>
/// Serialize object to an IBufferWriter for zero-copy scenarios. /// Serialize object to an IBufferWriter for zero-copy scenarios.
/// Uses BufferWriterBinaryOutput — writes directly to the caller's buffer. /// Uses BufferWriterBinaryOutput — writes directly to the caller's buffer.