Optimize string interning and context state checks

Pre-compute InternBit and reference handling flags in context to avoid repeated field access and shifting. Refactor all intern mode checks to use InternBit, improving performance and code clarity in generated code, property accessors, and scan/write passes. Optimize write plan entry access for faster serialization.
This commit is contained in:
Loretta 2026-03-01 13:31:45 +01:00
parent 0ff40a6777
commit 15da68fe25
6 changed files with 58 additions and 27 deletions

View File

@ -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}}}");
}

View File

@ -24,6 +24,16 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
public byte MaxDepth => Options.MaxDepth;
public ReferenceHandlingMode ReferenceHandling => Options.ReferenceHandling;
/// <summary>
/// Pre-computed: ReferenceHandling != None. Avoids Options field chain per call.
/// </summary>
private bool _hasRefHandling;
/// <summary>
/// Pre-computed: ReferenceHandling == IId (not All). When true, only IId types are tracked.
/// </summary>
private bool _hasIdHandling;
public bool ThrowOnCircularReference => Options.ThrowOnCircularReference;
/// <summary>
/// Global shared cache for metadata (thread-safe, shared across all contexts).
@ -48,17 +58,17 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
/// </summary>
protected abstract Func<Type, TMetadata> 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<TMetadata, TOptions>
public virtual void Reset(TOptions options)
{
Options = options;
_hasRefHandling = options.ReferenceHandling != ReferenceHandlingMode.None;
_hasIdHandling = options.ReferenceHandling == ReferenceHandlingMode.OnlyId;
}
/// <summary>

View File

@ -132,12 +132,15 @@ public static partial class AcBinarySerializer
/// <summary>
/// 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.
/// </summary>
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];
}
/// <summary>Write pass cursor index into sorted _writePlan array.</summary>
@ -146,6 +149,12 @@ public static partial class AcBinarySerializer
/// <summary>Write pass visit counter. Mirrors ScanVisitIndex ordering.</summary>
internal int WriteVisitIndex;
/// <summary>
/// Pre-loaded next write plan entry. Null when plan is empty or exhausted.
/// Avoids array access on hit path — entry is already cached.
/// </summary>
private WriteDuplicateEntry? _nextWritePlanEntry;
/// <summary>
/// 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;
/// <summary>
/// Pre-computed <c>1 &lt;&lt; (int)Options.UseStringInterning</c>.
/// 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.
/// </summary>
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

View File

@ -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)

View File

@ -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)

View File

@ -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;
}
/// <summary>
/// Object getter for property filter context.