diff --git a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs index 1cadf4e..46a4e92 100644 --- a/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs +++ b/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs @@ -636,11 +636,12 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (hasStringScan) { - // Hoist the shift once — per-property InterningFlags check uses internBit directly. + // Use pre-computed InternBit from context (avoids Options.UseStringInterning field chain + shift per object). + // Per-property InterningFlags check uses internBit directly. // Cannot combine flags (OR) because different properties may have different flags // and Attribute mode must NOT scan All-only properties. sb.AppendLine(); - sb.AppendLine(" var internBit = 1 << (int)context.Options.UseStringInterning;"); + sb.AppendLine(" var internBit = context.InternBit;"); sb.AppendLine(" int minIntern = 0, maxIntern = 0;"); sb.AppendLine(" if (internBit > 1) { minIntern = context.MinStringInternLength; maxIntern = context.MaxStringInternLength; }"); } @@ -722,7 +723,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (p.InterningFlags == 0) sb.AppendLine($"{i}context.StringInternEligible = false;"); else - sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;"); + sb.AppendLine($"{i}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); sb.AppendLine($"{i}AcBinarySerializer.WriteStringGenerated({a}, context);"); break; case PropertyTypeKind.Complex: @@ -1616,7 +1617,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (p.InterningFlags == 0) sb.AppendLine($"{ii}context.StringInternEligible = false;"); else - sb.AppendLine($"{ii}context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;"); + sb.AppendLine($"{ii}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); sb.AppendLine($"{ii}AcBinarySerializer.WriteStringGenerated({k}, context);"); } else if (IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum) @@ -1638,7 +1639,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator if (p.InterningFlags == 0) sb.AppendLine($"{ii} context.StringInternEligible = false;"); else - sb.AppendLine($"{ii} context.StringInternEligible = ({p.InterningFlags} & (1 << (int)context.Options.UseStringInterning)) != 0;"); + sb.AppendLine($"{ii} context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;"); sb.AppendLine($"{ii} AcBinarySerializer.WriteStringGenerated({v}, context);"); sb.AppendLine($"{ii}}}"); } diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index 574c344..71d1528 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -24,6 +24,16 @@ public abstract class AcSerializerContextBase public byte MaxDepth => Options.MaxDepth; public ReferenceHandlingMode ReferenceHandling => Options.ReferenceHandling; + + /// + /// Pre-computed: ReferenceHandling != None. Avoids Options field chain per call. + /// + private bool _hasRefHandling; + + /// + /// Pre-computed: ReferenceHandling == IId (not All). When true, only IId types are tracked. + /// + private bool _hasIdHandling; public bool ThrowOnCircularReference => Options.ThrowOnCircularReference; /// /// Global shared cache for metadata (thread-safe, shared across all contexts). @@ -48,17 +58,17 @@ public abstract class AcSerializerContextBase /// protected abstract Func MetadataFactory { get; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool UseTypeReferenceHandling(Type type) - { - var wrapper = GetWrapper(type); - return UseTypeReferenceHandling(wrapper.Metadata); - } + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public bool UseTypeReferenceHandling(Type type) + //{ + // var wrapper = GetWrapper(type); + // return UseTypeReferenceHandling(wrapper.Metadata); + //} [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool UseTypeReferenceHandling(TMetadata metaData) { - return ReferenceHandling != ReferenceHandlingMode.None && (metaData.IsIId || ReferenceHandling == ReferenceHandlingMode.All); + return _hasRefHandling && (metaData.IsIId || !_hasIdHandling); } #region Wrapper Access @@ -153,6 +163,8 @@ public abstract class AcSerializerContextBase public virtual void Reset(TOptions options) { Options = options; + _hasRefHandling = options.ReferenceHandling != ReferenceHandlingMode.None; + _hasIdHandling = options.ReferenceHandling == ReferenceHandlingMode.OnlyId; } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 051193c..712df90 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -132,12 +132,15 @@ public static partial class AcBinarySerializer /// /// Sorts write plan by VisitIndex for sequential cursor consumption in write pass. - /// Called once after scan pass completes. + /// Called once after scan pass completes. Pre-loads first entry for TryConsumeWritePlanEntry. /// internal void SortWritePlan() { if (_writePlanCount > 1) _writePlan.AsSpan(0, _writePlanCount).Sort(static (a, b) => a.VisitIndex.CompareTo(b.VisitIndex)); + + if (_writePlanCount > 0) + _nextWritePlanEntry = _writePlan![0]; } /// Write pass cursor index into sorted _writePlan array. @@ -146,6 +149,12 @@ public static partial class AcBinarySerializer /// Write pass visit counter. Mirrors ScanVisitIndex ordering. internal int WriteVisitIndex; + /// + /// Pre-loaded next write plan entry. Null when plan is empty or exhausted. + /// Avoids array access on hit path — entry is already cached. + /// + private WriteDuplicateEntry? _nextWritePlanEntry; + /// /// Set per-property in WritePropertyOrSkip before calling WriteString. /// Controls whether the current string property participates in the cursor-based interning. @@ -162,15 +171,14 @@ public static partial class AcBinarySerializer internal bool TryConsumeWritePlanEntry(out WriteDuplicateEntry entry) { var visitIndex = WriteVisitIndex++; - if (_writePlan != null && WritePlanCursor < _writePlanCount) + var next = _nextWritePlanEntry; + if (next != null && visitIndex == next.Value.VisitIndex) { - ref var candidate = ref _writePlan[WritePlanCursor]; - if (candidate.VisitIndex == visitIndex) - { - entry = candidate; - WritePlanCursor++; - return true; - } + entry = next.Value; + _nextWritePlanEntry = ++WritePlanCursor < _writePlanCount + ? _writePlan![WritePlanCursor] + : null; + return true; } entry = default; return false; @@ -209,6 +217,14 @@ public static partial class AcBinarySerializer // These properties delegate to Options for convenience internal bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None; + /// + /// Pre-computed 1 << (int)Options.UseStringInterning. + /// Used by sgen scan (per-object intern check) and sgen write (per-property StringInternEligible). + /// Avoids repeated field chain traversal (context.Options.UseStringInterning) + shift per call site. + /// Value: None=1, Attribute=2, All=4. + /// + public int InternBit { get; private set; } + public bool IsValidForInterningString(int strLength) { return strLength >= MinStringInternLength && (MaxStringInternLength == 0 || strLength <= MaxStringInternLength); @@ -265,6 +281,7 @@ public static partial class AcBinarySerializer // IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties base.Reset(options); HasPropertyFilter = Options.PropertyFilter != null; + InternBit = 1 << (int)Options.UseStringInterning; //FastWire = Options.WireMode == WireMode.Fast; } @@ -283,6 +300,7 @@ public static partial class AcBinarySerializer ScanVisitIndex = 0; WritePlanCursor = 0; WriteVisitIndex = 0; + _nextWritePlanEntry = null; StringInternEligible = false; // Clear write plan string references to avoid GC pinning, keep array if small enough diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index a50d98a..4f34741 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -173,7 +173,7 @@ public static partial class AcBinarySerializer if (prop.AccessorType == PropertyAccessorType.String) { - if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue; + if (!prop.UseStringPropertyInterning(context.InternBit)) continue; var str2 = prop.GetString(value); if (str2 != null && context.IsValidForInterningString(str2.Length)) @@ -181,7 +181,7 @@ public static partial class AcBinarySerializer } else if (prop.IsStringCollectionProperty) { - if (!prop.UseStringPropertyInterning(context.Options.UseStringInterning)) continue; + if (!prop.UseStringPropertyInterning(context.InternBit)) continue; var propValue = prop.GetValue(value); if (propValue != null) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 938b710..d6ab191 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1453,7 +1453,7 @@ public static partial class AcBinarySerializer } else { - context.StringInternEligible = prop.UseStringPropertyInterning(context.Options.UseStringInterning); + context.StringInternEligible = prop.UseStringPropertyInterning(context.InternBit); WriteString(value, context); } return; @@ -1463,7 +1463,7 @@ public static partial class AcBinarySerializer // Object type (collection, complex object, byte[], dictionary) // Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism // Set interning eligibility for string collection elements - context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.Options.UseStringInterning); + context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.InternBit); var value = prop.GetValue(obj); // SKIP marker only for null (reference types) diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs index a25bfd9..ac453e7 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -43,9 +43,9 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase private readonly byte _interningFlags; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool UseStringPropertyInterning(StringInterningMode stringInterningMode) + public bool UseStringPropertyInterning(int internBit) { - return (_interningFlags & (1 << (int)stringInterningMode)) != 0; + return (_interningFlags & internBit) != 0; } /// /// Object getter for property filter context.