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:
parent
0ff40a6777
commit
15da68fe25
|
|
@ -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}}}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 << (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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue