Optimize AcBinary hot paths with pre-cached type wrappers

Pre-caches TypeMetadataWrapper instances for complex properties,
eliminating repeated GetWrapper dictionary lookups in serialization
and deserialization. Adds ComplexPropertyIndex and ComplexPropertyCount
fields, and PropertyTypeWrappers array to TypeMetadataWrapper. Refactors
scan, write, and populate passes to use cached wrappers, improving
performance for deep and polymorphic object graphs. Updates benchmarks
to focus on FastMode variants. No breaking changes; internal efficiency
improved.
This commit is contained in:
Loretta 2026-02-14 01:41:54 +01:00
parent a0a6ac8ef4
commit 7e7918e071
11 changed files with 296 additions and 71 deletions

View File

@ -212,15 +212,15 @@ public static class Program
{
// AcBinary variants
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
//new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
// AcJson
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),

View File

@ -35,10 +35,15 @@ public static partial class AcBinaryDeserializer
var orderedProperties = WritableProperties;
PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length];
var complexCount = 0;
for (var i = 0; i < orderedProperties.Length; i++)
{
PropertiesArray[i] = new BinaryPropertySetterInfo(orderedProperties[i], type);
var setter = new BinaryPropertySetterInfo(orderedProperties[i], type);
if (setter.IsComplexType)
setter.ComplexPropertyIndex = complexCount++;
PropertiesArray[i] = setter;
}
ComplexPropertyCount = complexCount;
// Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute
if (false && type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))

View File

@ -18,8 +18,45 @@ public static partial class AcBinaryDeserializer
#endregion
/// <summary>
/// Loop-invariant state for PopulatePropertyWithMarker. Built once per object, passed by ref.
/// Reduces call-site overhead from 9 parameters to 3 (ref state + propInfo + propertyIndex).
/// </summary>
private ref struct PopulateState<TInput> where TInput : struct, IBinaryInputBase
{
public BinaryDeserializationContext<TInput> Context;
public object Target;
public TypeMetadataWrapper<BinaryDeserializeTypeMetadata> ParentWrapper;
public BinaryDeserializeTypeMetadata Metadata;
public int NextDepth;
public int Depth;
public bool IsMergeMode;
public bool SkipDefaultWrite;
}
#region Populate Object Methods
/// <summary>
/// Resolves a property type wrapper using PropertyTypeWrappers cache.
/// Falls back to GetWrapper on cache miss and populates the cache.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeMetadataWrapper<BinaryDeserializeTypeMetadata> ResolvePropertyWrapper<TInput>(
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> parentWrapper,
int complexPropertyIndex,
Type propertyType,
BinaryDeserializationContext<TInput> context)
where TInput : struct, IBinaryInputBase
{
var cached = parentWrapper.GetPropertyTypeWrapper(complexPropertyIndex, propertyType);
if (cached != null)
return cached;
var resolved = context.GetWrapper(propertyType);
parentWrapper.SetPropertyTypeWrapper(complexPropertyIndex, resolved);
return resolved;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
=> null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
@ -73,13 +110,23 @@ public static partial class AcBinaryDeserializer
var metadata = wrapper.Metadata;
var properties = metadata.PropertiesArray;
var cacheMap = wrapper.CacheMap;
var nextDepth = depth + 1;
var isMergeMode = context.IsMergeMode;
// UseMetadata: cacheMap.Length a source property-k száma
// Non-UseMetadata: properties.Length a target property-k száma (source == target)
var propCount = cacheMap?.Length ?? properties.Length;
var state = new PopulateState<TInput>
{
Context = context,
Target = target,
ParentWrapper = wrapper,
Metadata = metadata,
NextDepth = depth + 1,
Depth = depth,
IsMergeMode = context.IsMergeMode,
SkipDefaultWrite = skipDefaultWrite
};
if (!context.HasMetadata)
{
// Markerless loop: properties with ExpectedTypeCode read raw values directly.
@ -95,7 +142,7 @@ public static partial class AcBinaryDeserializer
}
// Non-markerless properties: standard marker-based read
PopulatePropertyWithMarker(context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
PopulatePropertyWithMarker(ref state, propInfo, i);
}
}
else
@ -105,32 +152,29 @@ public static partial class AcBinaryDeserializer
{
var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
PopulatePropertyWithMarker(context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
PopulatePropertyWithMarker(ref state, propInfo, i);
}
}
}
/// <summary>
/// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
/// Loop-invariant state is passed via ref PopulateState to reduce call-site overhead.
/// </summary>
private static void PopulatePropertyWithMarker<TInput>(
BinaryDeserializationContext<TInput> context,
object target,
ref PopulateState<TInput> state,
BinaryPropertySetterBase? propInfo,
BinaryDeserializeTypeMetadata metadata,
int nextDepth,
bool isMergeMode,
bool skipDefaultWrite,
int propertyIndex,
int depth)
int propertyIndex)
where TInput : struct, IBinaryInputBase
{
var context = state.Context;
var target = state.Target;
var peekCode = context.PeekByte();
// Nincs megfelelő target property → skip
if (propInfo == null)
{
SkipValue(context, metadata);
SkipValue(context, state.Metadata);
return;
}
@ -141,7 +185,7 @@ public static partial class AcBinaryDeserializer
// Populate mode: overwrite with default (existing object may have non-default values)
// Deserialize mode: skip write (new object already has defaults from CreateInstance)
if (!skipDefaultWrite)
if (!state.SkipDefaultWrite)
{
SetPropertyToDefault(target, propInfo);
}
@ -156,6 +200,8 @@ public static partial class AcBinaryDeserializer
return;
}
var nextDepth = state.NextDepth;
// Handle collections
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
{
@ -165,7 +211,7 @@ public static partial class AcBinaryDeserializer
context.ReadByte(); // consume Array marker
// Merge mode with IId collection: use merge logic
if (isMergeMode && propInfo.IsIIdCollection)
if (state.IsMergeMode && propInfo.IsIIdCollection)
{
MergeIIdCollection(context, existingList, propInfo, nextDepth);
}
@ -188,8 +234,12 @@ public static partial class AcBinaryDeserializer
var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth);
if (nestedValue != null)
{
var nestedMeta = context.GetWrapper(propInfo.PropertyType).Metadata;
CopyProperties(nestedValue, existingObj, nestedMeta);
var complexIdx = propInfo.ComplexPropertyIndex;
var parentWrapper = state.ParentWrapper;
var nestedWrapper = complexIdx >= 0
? ResolvePropertyWrapper(parentWrapper, complexIdx, propInfo.PropertyType, context)
: context.GetWrapper(propInfo.PropertyType);
CopyProperties(nestedValue, existingObj, nestedWrapper.Metadata);
}
return;
}
@ -204,8 +254,20 @@ public static partial class AcBinaryDeserializer
TryReadAndSetTypedValue(context, target, propInfo, peekCode))
return;
var value = ReadValue(context, propInfo.PropertyType, nextDepth);
propInfo.SetValue(target, value);
// Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup
var complexIdx = propInfo.ComplexPropertyIndex;
if (complexIdx >= 0 && peekCode == BinaryTypeCode.Object)
{
context.ReadByte(); // consume Object marker
var propWrapper = ResolvePropertyWrapper(state.ParentWrapper, complexIdx, propInfo.PropertyType, context);
var value = ReadObjectCoreWithWrapper(context, propWrapper, nextDepth, cacheIndex: -1);
propInfo.SetValue(target, value);
}
else
{
var value = ReadValue(context, propInfo.PropertyType, nextDepth);
propInfo.SetValue(target, value);
}
}
catch (InvalidCastException ex)
{
@ -215,9 +277,9 @@ public static partial class AcBinaryDeserializer
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
$"Depth: {depth}. " +
$"Depth: {state.Depth}. " +
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
$"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
$"All target properties: [{string.Join(", ", state.Metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
$"Error: {ex.Message}",
positionBeforeRead,
propInfo.PropertyType,
@ -360,8 +422,17 @@ public static partial class AcBinaryDeserializer
}
}
// Read new value
var value = ReadValue(context, elementType, nextDepth);
// Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
object? value;
if (peekCode == BinaryTypeCode.Object && elementMetadata != null)
{
context.ReadByte(); // consume Object marker
value = ReadObjectCoreWithWrapper(context, wrapper, nextDepth, cacheIndex: -1);
}
else
{
value = ReadValue(context, elementType, nextDepth);
}
if (i < existingCount)
{
@ -436,27 +507,30 @@ public static partial class AcBinaryDeserializer
for (int i = 0; i < arrayCount; i++)
{
var itemCode = context.PeekByte();
if (itemCode != BinaryTypeCode.Object)
// Read or create the new item
object? newItem;
if (itemCode == BinaryTypeCode.Object)
{
var value = ReadValue(context, elementType, nextDepth);
if (value != null)
existingList.Add(value);
continue;
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
context.ReadByte(); // consume Object marker
newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
}
else
{
// Fallback for non-Object markers (null, ObjectRef, etc.)
newItem = ReadValue(context, elementType, nextDepth);
if (newItem == null) continue;
}
context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
// PopulateObjectCore handles hashcode reading for Non-IId types
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType))
{
// Track this ID as seen in source
sourceIds?.Add(itemId);
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
{
// Copy properties to existing item (preserves reference)
@ -532,7 +606,7 @@ public static partial class AcBinaryDeserializer
var arrayCount = (int)context.ReadVarUInt();
var nextDepth = depth + 1;
// Track which IDs we see in source (for orphan removal)
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
? new HashSet<object>(arrayCount)
@ -541,20 +615,23 @@ public static partial class AcBinaryDeserializer
for (int i = 0; i < arrayCount; i++)
{
var itemCode = context.PeekByte();
if (itemCode != BinaryTypeCode.Object)
// Read or create the new item
object? newItem;
if (itemCode == BinaryTypeCode.Object)
{
var value = ReadValue(context, elementType, nextDepth);
if (value != null)
existingList.Add(value);
continue;
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
context.ReadByte(); // consume Object marker
newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
}
else
{
// Fallback for non-Object markers (null, ObjectRef, etc.)
newItem = ReadValue(context, elementType, nextDepth);
if (newItem == null) continue;
}
context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
// PopulateObjectCore handles hashcode reading for Non-IId types
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType))

View File

@ -1116,7 +1116,17 @@ public static partial class AcBinaryDeserializer
}
var wrapper = context.GetWrapper(targetType);
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
/// <summary>
/// Object olvasás with pre-resolved wrapper (eliminates GetWrapper dictionary lookup).
/// </summary>
private static object? ReadObjectCoreWithWrapper<TInput>(BinaryDeserializationContext<TInput> context, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth, int cacheIndex)
where TInput : struct, IBinaryInputBase
{
var metadata = wrapper.Metadata;
var targetType = metadata.SourceType;
var instance = CreateInstance(targetType, metadata);
if (instance == null) return null;

View File

@ -75,11 +75,13 @@ public static partial class AcBinarySerializer
private BinaryPropertyAccessor[] ComputeReferenceProperties()
{
var list = new List<BinaryPropertyAccessor>();
foreach (var prop in Properties)
for (var i = 0; i < Properties.Length; i++)
{
var prop = Properties[i];
if (prop.IsComplexType || prop.AccessorType == PropertyAccessorType.String)
list.Add(prop);
}
return list.ToArray();
}
@ -120,21 +122,22 @@ public static partial class AcBinarySerializer
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
var complexCount = 0;
for (var i = 0; i < orderedProperties.Length; i++)
{
var accessor = new BinaryPropertyAccessor(orderedProperties[i], type);
accessor.PropertyIndex = i;
Properties[i] = accessor;
// Count complex properties using pre-computed IsComplexType
// Assign ComplexPropertyIndex for non-primitive, non-string properties
if (accessor.IsComplexType)
{
complexCount++;
accessor.ComplexPropertyIndex = complexCount++;
}
}
// Set scan optimization flags
ComplexPropertyCount = complexCount;
HasComplexProperties = complexCount > 0;
// Type needs reference tracking if:

View File

@ -127,11 +127,17 @@ public static partial class AcBinarySerializer
}
else
{
// Object property: use generic getter, get wrapper for property type
// Object property: use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
var propValue = prop.GetValue(value);
if (propValue != null)
{
var propWrapper = context.GetWrapper(prop.PropertyType);
var runtimeType = propValue.GetType();
var propWrapper = wrapper.GetPropertyTypeWrapper(prop.ComplexPropertyIndex, runtimeType);
if (propWrapper == null)
{
propWrapper = context.GetWrapper(runtimeType);
wrapper.SetPropertyTypeWrapper(prop.ComplexPropertyIndex, propWrapper);
}
ScanValue(propValue, propWrapper, context, nextDepth2);
}
}

View File

@ -497,6 +497,53 @@ public static partial class AcBinarySerializer
WriteObject(value, wrapper, context, depth, isNested: depth > 0);
}
/// <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.
/// </summary>
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var type = wrapper.Metadata.SourceType;
// Nullable<T> where T is a value type: boxed value may be a primitive.
if (type.IsValueType)
{
if (TryWritePrimitive(value, value.GetType(), context))
return;
}
if (depth > context.MaxDepth)
{
context.WriteByte(BinaryTypeCode.Null);
return;
}
// Handle byte arrays specially (value-like, no reference tracking)
if (value is byte[] byteArray)
{
WriteByteArray(byteArray, context);
return;
}
// Handle dictionaries
if (value is IDictionary dictionary)
{
WriteDictionary(dictionary, context, depth);
return;
}
// Handle collections/arrays
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{
WriteArray(enumerable, wrapper, context, depth);
return;
}
// Handle complex objects with single-pass reference tracking
WriteObject(value, wrapper, context, depth, isNested: depth > 0);
}
/// <summary>
/// Optimized primitive writer using TypeCode dispatch.
/// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
@ -1017,7 +1064,7 @@ public static partial class AcBinarySerializer
}
else
{
WritePropertyOrSkip(value, prop, context, nextDepth);
WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
}
}
}
@ -1034,7 +1081,7 @@ public static partial class AcBinarySerializer
continue;
}
WritePropertyOrSkip(value, prop, context, nextDepth);
WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
}
}
}
@ -1172,7 +1219,7 @@ public static partial class AcBinarySerializer
/// Avoids double getter calls.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, BinarySerializationContext<TOutput> context, int depth)
private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper<BinarySerializeTypeMetadata> parentWrapper, BinarySerializationContext<TOutput> context, int depth)
where TOutput : struct, IBinaryOutputBase
{
switch (prop.AccessorType)
@ -1335,7 +1382,7 @@ public static partial class AcBinarySerializer
default:
{
// Object type (collection, complex object, byte[], dictionary)
// TryWritePrimitive is always false for these — skip it via WriteValueNonPrimitive
// Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
var value = prop.GetValue(obj);
// SKIP marker only for null (reference types)
@ -1349,7 +1396,23 @@ public static partial class AcBinarySerializer
#if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif
WriteValueNonPrimitive(value, prop.PropertyType, context, depth);
var runtimeType = value.GetType();
var complexIdx = prop.ComplexPropertyIndex;
if (complexIdx >= 0)
{
var propWrapper = parentWrapper.GetPropertyTypeWrapper(complexIdx, runtimeType);
if (propWrapper == null)
{
propWrapper = context.GetWrapper(runtimeType);
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
}
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
}
else
{
// Non-complex in default case (nullable value type, etc.)
WriteValueNonPrimitive(value, runtimeType, context, depth);
}
}
return;
}

View File

@ -18,6 +18,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
/// </summary>
public int PropertyIndex { get; internal set; } = -1;
/// <summary>
/// Index into the PropertyTypeWrappers array on TypeMetadataWrapper.
/// Only set for complex (non-primitive, non-string) properties. -1 for primitives and strings.
/// Used to pre-cache TypeMetadataWrapper per property type, eliminating GetWrapper dictionary lookups.
/// </summary>
public int ComplexPropertyIndex { get; internal set; } = -1;
/// <summary>
/// Cached string intern attribute value for this property.
/// null = no attribute (use global StringInterningMode setting)

View File

@ -12,6 +12,13 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary>
public abstract class BinaryPropertySetterBase : PropertySetterBase
{
/// <summary>
/// Index into the PropertyTypeWrappers array on TypeMetadataWrapper.
/// Only set for complex (non-primitive, non-string) properties. -1 for primitives and strings.
/// Used to pre-cache TypeMetadataWrapper per property type, eliminating GetWrapper dictionary lookups.
/// </summary>
public int ComplexPropertyIndex { get; internal set; } = -1;
/// <summary>
/// Whether this property is a complex type (not primitive, string, enum, or common value types).
/// Note: Shadows PropertyAccessorBase.IsComplexType with Binary-specific check.

View File

@ -116,6 +116,12 @@ public abstract class TypeMetadataBase
/// </summary>
public bool HasComplexProperties { get; protected set; }
/// <summary>
/// Number of complex (non-primitive, non-string) properties.
/// Used to allocate PropertyTypeWrappers array on TypeMetadataWrapper.
/// </summary>
public int ComplexPropertyCount { get; protected set; }
public bool IsComplexType { get; init; }
/// <summary>

View File

@ -50,6 +50,14 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary>
internal BinaryPropertySetterBase?[]? CacheMap;
/// <summary>
/// Pre-cached wrappers for complex (non-primitive, non-string) property types.
/// Indexed by BinaryPropertyAccessorBase.ComplexPropertyIndex.
/// Eliminates GetWrapper dictionary lookups in scan and write pass hot paths.
/// Lazy-allocated on first access, cleared in ResetTracking.
/// </summary>
internal TypeMetadataWrapper<TMetadata>?[]? PropertyTypeWrappers;
#region Typed IdentityMaps - No generic type checks in hot path!
/// <summary>
@ -109,6 +117,10 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
break;
}
// Pre-allocate PropertyTypeWrappers — eliminates null/resize checks from hot path
if (metadata.ComplexPropertyCount > 0)
PropertyTypeWrappers = new TypeMetadataWrapper<TMetadata>?[metadata.ComplexPropertyCount];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -117,6 +129,35 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
return referenceHandling != ReferenceHandlingMode.None && (Metadata.IsIId || referenceHandling == ReferenceHandlingMode.All);
}
/// <summary>
/// Gets a pre-cached wrapper for a complex property type.
/// Returns the cached wrapper if it matches the runtime type, null otherwise.
/// </summary>
/// <param name="complexPropertyIndex">Index from BinaryPropertyAccessorBase.ComplexPropertyIndex (-1 for non-complex)</param>
/// <param name="runtimeType">The actual runtime type of the property value</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeMetadataWrapper<TMetadata>? GetPropertyTypeWrapper(int complexPropertyIndex, Type runtimeType)
{
if (complexPropertyIndex < 0)
return null;
var cached = PropertyTypeWrappers![complexPropertyIndex];
if (cached != null && cached.Metadata.SourceType == runtimeType)
return cached;
return null;
}
/// <summary>
/// Caches a resolved wrapper for a complex property type.
/// Overwrites any previous value (handles polymorphic types gracefully).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetPropertyTypeWrapper(int complexPropertyIndex, TypeMetadataWrapper<TMetadata> wrapper)
{
PropertyTypeWrappers![complexPropertyIndex] = wrapper;
}
/// <summary>
/// Resets tracking state for reuse between serializations.
/// Does not deallocate - just clears for reuse (pool-friendly).