Refactor polymorphic and ref handling in serializer
Split hot/cold paths for polymorphic and reference-tracked object serialization. Introduce dedicated cold-path methods for polymorphic and IId reference handling, and new helpers for writing reference indices. Extract property writing loop for reuse. Simplify WriteObject and update property dispatch logic to reduce branching and clarify marker handling.
This commit is contained in:
parent
68c25b2381
commit
76ce60b7f0
|
|
@ -661,9 +661,8 @@ public static partial class AcBinarySerializer
|
|||
/// <summary>
|
||||
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
|
||||
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
|
||||
/// When polyRuntimeType is set, writes polymorphic prefix/combined markers.
|
||||
/// </summary>
|
||||
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type? polyRuntimeType = null)
|
||||
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var type = wrapper.Metadata.SourceType;
|
||||
|
|
@ -677,7 +676,6 @@ public static partial class AcBinarySerializer
|
|||
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
return;
|
||||
}
|
||||
|
|
@ -685,7 +683,6 @@ public static partial class AcBinarySerializer
|
|||
// Handle byte arrays specially (value-like, no reference tracking)
|
||||
if (value is byte[] byteArray)
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteByteArray(byteArray, context);
|
||||
return;
|
||||
}
|
||||
|
|
@ -693,7 +690,6 @@ public static partial class AcBinarySerializer
|
|||
// Handle dictionaries
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteDictionary(dictionary, context, depth);
|
||||
return;
|
||||
}
|
||||
|
|
@ -701,13 +697,61 @@ public static partial class AcBinarySerializer
|
|||
// Handle collections/arrays
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteArray(enumerable, wrapper, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle complex objects — combined poly+ref markers handled inside WriteObject
|
||||
WriteObject(value, wrapper, context, depth, polyRuntimeType);
|
||||
// Handle complex objects
|
||||
WriteObject(value, wrapper, context, depth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polymorphic variant of WriteValueNonPrimitiveWithWrapper.
|
||||
/// Cold path: polymorphism is rare. Writes poly prefix for non-object types,
|
||||
/// delegates to WriteObjectPolymorphic for combined poly+ref marker handling.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteValueNonPrimitiveWithWrapperPoly<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type polyRuntimeType)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var type = wrapper.Metadata.SourceType;
|
||||
|
||||
if (type.IsValueType)
|
||||
{
|
||||
if (TryWritePrimitive(value, value.GetType(), context))
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is byte[] byteArray)
|
||||
{
|
||||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteByteArray(byteArray, context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteDictionary(dictionary, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteArray(enumerable, wrapper, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Complex object — handles combined poly+ref markers
|
||||
WriteObjectPolymorphic(value, wrapper, context, depth, polyRuntimeType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1025,9 +1069,7 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
else
|
||||
{
|
||||
// StringRef: write index reference only (no getter call, no string data)
|
||||
context.WriteByte(BinaryTypeCode.StringInterned);
|
||||
context.WriteVarUInt((uint)planEntry.CacheMapIndex);
|
||||
WriteStringInternRef(context, planEntry.CacheMapIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1069,101 +1111,173 @@ public static partial class AcBinarySerializer
|
|||
context.WriteBytes(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// String intern 2nd occurrence — cold path, just writes reference index.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteStringInternRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.StringInterned);
|
||||
context.WriteVarUInt((uint)cacheMapIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Object ref 2nd occurrence — cold path, just writes reference index.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteObjectRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarUInt((uint)cacheMapIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Complex Type Writers
|
||||
|
||||
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type? polyRuntimeType = null)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Per-type metadata flag: when EnableMetadataFeature=false on [AcBinarySerializable],
|
||||
// skip inline metadata and use markerless property write — even when global UseMetadata=true.
|
||||
// Deserializer must have the same attribute on the type (developer responsibility).
|
||||
var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature;
|
||||
|
||||
// UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
|
||||
var isFirstMetadataOccurrence = false;
|
||||
if (useMetaForType)
|
||||
{
|
||||
isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
|
||||
}
|
||||
|
||||
// Reference handling: consume pre-computed write plan entry from scan pass cursor
|
||||
var cachedObjectCacheIndex = -1; // -1 = not cached, 0+ = cache index for first write
|
||||
// Only IId types with ref handling enabled go to cold path
|
||||
if (context.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
||||
{
|
||||
ValidateWritePlanObject(in planEntry, value, wrapper);
|
||||
if (planEntry.IsFirst)
|
||||
{
|
||||
// First occurrence of a cached IId object — write full object + cache index
|
||||
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2+ occurrence → write ObjectRef directly (no poly prefix needed —
|
||||
// object already in cache, deser knows the type)
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarUInt((uint)planEntry.CacheMapIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (useMetaForType)
|
||||
WriteObjectWithRefHandlingMeta(value, wrapper, context, depth);
|
||||
else
|
||||
WriteObjectWithRefHandling(value, wrapper, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Marker kiírása — polymorphic vs non-polymorphic paths
|
||||
if (polyRuntimeType != null)
|
||||
if (useMetaForType)
|
||||
{
|
||||
WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
|
||||
}
|
||||
else if (useMetaForType)
|
||||
{
|
||||
if (cachedObjectCacheIndex >= 0)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
|
||||
context.WriteVarUInt((uint)cachedObjectCacheIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
|
||||
}
|
||||
// UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
|
||||
var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
|
||||
|
||||
// Marker kiírása — no ref handling, no cachedObjectCacheIndex
|
||||
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
|
||||
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cachedObjectCacheIndex >= 0)
|
||||
// FixObj: assign slot on first occurrence this session
|
||||
if (!wrapper.PolymorphicSeen)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRefFirst);
|
||||
context.WriteVarUInt((uint)cachedObjectCacheIndex);
|
||||
wrapper.PolymorphicSeen = true;
|
||||
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
|
||||
}
|
||||
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
||||
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
|
||||
else
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
}
|
||||
|
||||
WriteObjectProperties(value, metadata, wrapper, context, depth, useMetaForType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WriteObject variant with reference handling, no metadata.
|
||||
/// Cold path: only IId types with ref tracking enabled.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteObjectWithRefHandling<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
// Reference handling: consume pre-computed write plan entry from scan pass cursor
|
||||
var cachedObjectCacheIndex = -1;
|
||||
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
||||
{
|
||||
ValidateWritePlanObject(in planEntry, value, wrapper);
|
||||
if (planEntry.IsFirst)
|
||||
{
|
||||
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// FixObj: assign slot on first occurrence this session
|
||||
if (!wrapper.PolymorphicSeen)
|
||||
{
|
||||
wrapper.PolymorphicSeen = true;
|
||||
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
|
||||
}
|
||||
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
||||
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
|
||||
else
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
WriteObjectRef(context, planEntry.CacheMapIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Write all properties (startIndex=0, including Id for IId types)
|
||||
// Marker kiírása — no metadata
|
||||
if (cachedObjectCacheIndex >= 0)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRefFirst);
|
||||
context.WriteVarUInt((uint)cachedObjectCacheIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!wrapper.PolymorphicSeen)
|
||||
{
|
||||
wrapper.PolymorphicSeen = true;
|
||||
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
|
||||
}
|
||||
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
|
||||
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
|
||||
else
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
}
|
||||
|
||||
WriteObjectProperties(value, wrapper.Metadata, wrapper, context, depth, useMetaForType: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WriteObject variant with reference handling + metadata.
|
||||
/// Cold path: IId types with ref tracking + UseMetadata enabled.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteObjectWithRefHandlingMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);
|
||||
|
||||
// Reference handling: consume pre-computed write plan entry from scan pass cursor
|
||||
var cachedObjectCacheIndex = -1;
|
||||
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
||||
{
|
||||
ValidateWritePlanObject(in planEntry, value, wrapper);
|
||||
if (planEntry.IsFirst)
|
||||
{
|
||||
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteObjectRef(context, planEntry.CacheMapIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Marker kiírása — with metadata
|
||||
if (cachedObjectCacheIndex >= 0)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
|
||||
context.WriteVarUInt((uint)cachedObjectCacheIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
|
||||
}
|
||||
context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
|
||||
|
||||
WriteObjectProperties(value, wrapper.Metadata, wrapper, context, depth, useMetaForType: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared property writing loop — used by WriteObject, WriteObjectWithRefHandling, WriteObjectPolymorphic.
|
||||
/// </summary>
|
||||
private static void WriteObjectProperties<TOutput>(object value, BinarySerializeTypeMetadata metadata, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, bool useMetaForType)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var nextDepth = depth + 1;
|
||||
var properties = metadata.Properties;
|
||||
var propCount = properties.Length;
|
||||
var hasPropertyFilter = context.HasPropertyFilter;
|
||||
|
||||
// Source-generated fast path: bypass the entire switch/delegate loop.
|
||||
// Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties)
|
||||
// and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking.
|
||||
// String interning is safe: generated code uses pre-computed interningFlags bit-check
|
||||
// matching runtime UseStringPropertyInterning — cursor alignment guaranteed for all modes.
|
||||
if (context.UseGeneratedCode)
|
||||
{
|
||||
var generatedWriter = wrapper.GeneratedWriter;
|
||||
|
|
@ -1176,14 +1290,9 @@ public static partial class AcBinarySerializer
|
|||
|
||||
if (!useMetaForType)
|
||||
{
|
||||
// Markerless loop: no extra branching per property for the common case.
|
||||
// Properties with ExpectedTypeCode write raw values (no type marker, no skip).
|
||||
// Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path.
|
||||
// Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out).
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var prop = properties[i];
|
||||
//context.CurrentProperty = prop;
|
||||
|
||||
if (prop.ExpectedTypeCode.HasValue)
|
||||
{
|
||||
|
|
@ -1201,11 +1310,9 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
else
|
||||
{
|
||||
// UseMetadata=true loop — UNCHANGED, zero extra overhead
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var prop = properties[i];
|
||||
//context.CurrentProperty = prop;
|
||||
|
||||
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||
{
|
||||
|
|
@ -1251,6 +1358,42 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polymorphic object writing — handles combined poly+ref markers.
|
||||
/// Cold path: polymorphism is rare, NoInlining acceptable.
|
||||
/// Poly always implies UseMetadata=false (checked in WritePropertyOrSkip).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteObjectPolymorphic<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type polyRuntimeType)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
// Reference handling
|
||||
var cachedObjectCacheIndex = -1;
|
||||
if (context.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
if (context.TryConsumeWritePlanEntry(out var planEntry))
|
||||
{
|
||||
ValidateWritePlanObject(in planEntry, value, wrapper);
|
||||
if (planEntry.IsFirst)
|
||||
{
|
||||
cachedObjectCacheIndex = planEntry.CacheMapIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteObjectRef(context, planEntry.CacheMapIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poly marker (handles combined poly+ref)
|
||||
WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
|
||||
|
||||
WriteObjectProperties(value, metadata, wrapper, context, depth, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a property value is null or default without boxing for value types.
|
||||
/// </summary>
|
||||
|
|
@ -1560,12 +1703,6 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
var runtimeType = value.GetType();
|
||||
|
||||
// Polymorphic detection: when declared type ≠ runtime type, pass polyRuntimeType
|
||||
// to WriteValueNonPrimitiveWithWrapper → WriteObject for combined marker handling.
|
||||
// For collections: normal prefix pattern (68/70 + inner Array/Dict marker).
|
||||
// For objects: combined markers (69/71) when RefFirst, no prefix for ObjectRef.
|
||||
var isPoly = !context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType;
|
||||
|
||||
var complexIdx = prop.ComplexPropertyIndex;
|
||||
if (complexIdx >= 0)
|
||||
{
|
||||
|
|
@ -1575,12 +1712,16 @@ public static partial class AcBinarySerializer
|
|||
propWrapper = context.GetWrapper(runtimeType);
|
||||
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
|
||||
}
|
||||
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null);
|
||||
if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
|
||||
WriteValueNonPrimitiveWithWrapperPoly(value, propWrapper, context, depth, runtimeType);
|
||||
else
|
||||
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-complex in default case (nullable value type, etc.)
|
||||
if (isPoly) context.WritePolymorphicPrefix(runtimeType);
|
||||
if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
|
||||
context.WritePolymorphicPrefix(runtimeType);
|
||||
WriteValueNonPrimitive(value, runtimeType, context, depth);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue