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 // AcBinary variants
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef), //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.FastMode, SerializerAcBinaryFastMode), //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 // AcJson
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault), new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),

View File

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

View File

@ -18,8 +18,45 @@ public static partial class AcBinaryDeserializer
#endregion #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 #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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
=> null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); => null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
@ -73,13 +110,23 @@ public static partial class AcBinaryDeserializer
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
var properties = metadata.PropertiesArray; var properties = metadata.PropertiesArray;
var cacheMap = wrapper.CacheMap; var cacheMap = wrapper.CacheMap;
var nextDepth = depth + 1;
var isMergeMode = context.IsMergeMode;
// UseMetadata: cacheMap.Length a source property-k száma // UseMetadata: cacheMap.Length a source property-k száma
// Non-UseMetadata: properties.Length a target property-k száma (source == target) // Non-UseMetadata: properties.Length a target property-k száma (source == target)
var propCount = cacheMap?.Length ?? properties.Length; 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) if (!context.HasMetadata)
{ {
// Markerless loop: properties with ExpectedTypeCode read raw values directly. // 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 // Non-markerless properties: standard marker-based read
PopulatePropertyWithMarker(context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth); PopulatePropertyWithMarker(ref state, propInfo, i);
} }
} }
else else
@ -105,32 +152,29 @@ public static partial class AcBinaryDeserializer
{ {
var propInfo = cacheMap != null ? cacheMap[i] : properties[i]; var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
PopulatePropertyWithMarker(context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth); PopulatePropertyWithMarker(ref state, propInfo, i);
} }
} }
} }
/// <summary> /// <summary>
/// Standard marker-based property read. Extracted to avoid duplicating logic in both loops. /// 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> /// </summary>
private static void PopulatePropertyWithMarker<TInput>( private static void PopulatePropertyWithMarker<TInput>(
BinaryDeserializationContext<TInput> context, ref PopulateState<TInput> state,
object target,
BinaryPropertySetterBase? propInfo, BinaryPropertySetterBase? propInfo,
BinaryDeserializeTypeMetadata metadata, int propertyIndex)
int nextDepth,
bool isMergeMode,
bool skipDefaultWrite,
int propertyIndex,
int depth)
where TInput : struct, IBinaryInputBase where TInput : struct, IBinaryInputBase
{ {
var context = state.Context;
var target = state.Target;
var peekCode = context.PeekByte(); var peekCode = context.PeekByte();
// Nincs megfelelő target property → skip // Nincs megfelelő target property → skip
if (propInfo == null) if (propInfo == null)
{ {
SkipValue(context, metadata); SkipValue(context, state.Metadata);
return; return;
} }
@ -141,7 +185,7 @@ public static partial class AcBinaryDeserializer
// Populate mode: overwrite with default (existing object may have non-default values) // Populate mode: overwrite with default (existing object may have non-default values)
// Deserialize mode: skip write (new object already has defaults from CreateInstance) // Deserialize mode: skip write (new object already has defaults from CreateInstance)
if (!skipDefaultWrite) if (!state.SkipDefaultWrite)
{ {
SetPropertyToDefault(target, propInfo); SetPropertyToDefault(target, propInfo);
} }
@ -156,6 +200,8 @@ public static partial class AcBinaryDeserializer
return; return;
} }
var nextDepth = state.NextDepth;
// Handle collections // Handle collections
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
{ {
@ -165,7 +211,7 @@ public static partial class AcBinaryDeserializer
context.ReadByte(); // consume Array marker context.ReadByte(); // consume Array marker
// Merge mode with IId collection: use merge logic // Merge mode with IId collection: use merge logic
if (isMergeMode && propInfo.IsIIdCollection) if (state.IsMergeMode && propInfo.IsIIdCollection)
{ {
MergeIIdCollection(context, existingList, propInfo, nextDepth); MergeIIdCollection(context, existingList, propInfo, nextDepth);
} }
@ -188,8 +234,12 @@ public static partial class AcBinaryDeserializer
var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth); var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth);
if (nestedValue != null) if (nestedValue != null)
{ {
var nestedMeta = context.GetWrapper(propInfo.PropertyType).Metadata; var complexIdx = propInfo.ComplexPropertyIndex;
CopyProperties(nestedValue, existingObj, nestedMeta); var parentWrapper = state.ParentWrapper;
var nestedWrapper = complexIdx >= 0
? ResolvePropertyWrapper(parentWrapper, complexIdx, propInfo.PropertyType, context)
: context.GetWrapper(propInfo.PropertyType);
CopyProperties(nestedValue, existingObj, nestedWrapper.Metadata);
} }
return; return;
} }
@ -204,9 +254,21 @@ public static partial class AcBinaryDeserializer
TryReadAndSetTypedValue(context, target, propInfo, peekCode)) TryReadAndSetTypedValue(context, target, propInfo, peekCode))
return; return;
// 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); var value = ReadValue(context, propInfo.PropertyType, nextDepth);
propInfo.SetValue(target, value); propInfo.SetValue(target, value);
} }
}
catch (InvalidCastException ex) catch (InvalidCastException ex)
{ {
var targetType = target.GetType(); var targetType = target.GetType();
@ -215,9 +277,9 @@ public static partial class AcBinaryDeserializer
$"Expected type: '{propInfo.PropertyType.FullName}'. " + $"Expected type: '{propInfo.PropertyType.FullName}'. " +
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
$"Position before read: {positionBeforeRead}, current: {context.Position}. " + $"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}. " + $"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}", $"Error: {ex.Message}",
positionBeforeRead, positionBeforeRead,
propInfo.PropertyType, propInfo.PropertyType,
@ -360,8 +422,17 @@ public static partial class AcBinaryDeserializer
} }
} }
// Read new value // Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
var value = ReadValue(context, elementType, nextDepth); 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) if (i < existingCount)
{ {
@ -436,20 +507,23 @@ public static partial class AcBinaryDeserializer
for (int i = 0; i < arrayCount; i++) for (int i = 0; i < arrayCount; i++)
{ {
var itemCode = context.PeekByte(); 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); // Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
if (value != null)
existingList.Add(value);
continue;
}
context.ReadByte(); // consume Object marker context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata); newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue; if (newItem == null) continue;
// PopulateObjectCore handles hashcode reading for Non-IId types
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true); 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;
}
var itemId = idGetter(newItem); var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType)) if (itemId != null && !IsDefaultValue(itemId, idType))
@ -541,20 +615,23 @@ public static partial class AcBinaryDeserializer
for (int i = 0; i < arrayCount; i++) for (int i = 0; i < arrayCount; i++)
{ {
var itemCode = context.PeekByte(); 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); // Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
if (value != null)
existingList.Add(value);
continue;
}
context.ReadByte(); // consume Object marker context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata); newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue; if (newItem == null) continue;
// PopulateObjectCore handles hashcode reading for Non-IId types
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true); 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;
}
var itemId = idGetter(newItem); var itemId = idGetter(newItem);
if (itemId != null && !IsDefaultValue(itemId, idType)) if (itemId != null && !IsDefaultValue(itemId, idType))

View File

@ -1116,7 +1116,17 @@ public static partial class AcBinaryDeserializer
} }
var wrapper = context.GetWrapper(targetType); 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 metadata = wrapper.Metadata;
var targetType = metadata.SourceType;
var instance = CreateInstance(targetType, metadata); var instance = CreateInstance(targetType, metadata);
if (instance == null) return null; if (instance == null) return null;

View File

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

View File

@ -127,11 +127,17 @@ public static partial class AcBinarySerializer
} }
else 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); var propValue = prop.GetValue(value);
if (propValue != null) 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); ScanValue(propValue, propWrapper, context, nextDepth2);
} }
} }

View File

@ -497,6 +497,53 @@ public static partial class AcBinarySerializer
WriteObject(value, wrapper, context, depth, isNested: depth > 0); 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> /// <summary>
/// Optimized primitive writer using TypeCode dispatch. /// Optimized primitive writer using TypeCode dispatch.
/// Avoids Nullable.GetUnderlyingType in hot path by using cached type info. /// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
@ -1017,7 +1064,7 @@ public static partial class AcBinarySerializer
} }
else else
{ {
WritePropertyOrSkip(value, prop, context, nextDepth); WritePropertyOrSkip(value, prop, wrapper, context, nextDepth);
} }
} }
} }
@ -1034,7 +1081,7 @@ public static partial class AcBinarySerializer
continue; 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. /// Avoids double getter calls.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 where TOutput : struct, IBinaryOutputBase
{ {
switch (prop.AccessorType) switch (prop.AccessorType)
@ -1335,7 +1382,7 @@ public static partial class AcBinarySerializer
default: default:
{ {
// Object type (collection, complex object, byte[], dictionary) // 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); var value = prop.GetValue(obj);
// SKIP marker only for null (reference types) // SKIP marker only for null (reference types)
@ -1349,7 +1396,23 @@ public static partial class AcBinarySerializer
#if DEBUG #if DEBUG
context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}"; context.CurrentPropertyPath = $"{prop.DeclaringType.Name}.{prop.Name}";
#endif #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; return;
} }

View File

@ -18,6 +18,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
/// </summary> /// </summary>
public int PropertyIndex { get; internal set; } = -1; 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> /// <summary>
/// Cached string intern attribute value for this property. /// Cached string intern attribute value for this property.
/// null = no attribute (use global StringInterningMode setting) /// null = no attribute (use global StringInterningMode setting)

View File

@ -12,6 +12,13 @@ namespace AyCode.Core.Serializers.Binaries;
/// </summary> /// </summary>
public abstract class BinaryPropertySetterBase : PropertySetterBase 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> /// <summary>
/// Whether this property is a complex type (not primitive, string, enum, or common value types). /// Whether this property is a complex type (not primitive, string, enum, or common value types).
/// Note: Shadows PropertyAccessorBase.IsComplexType with Binary-specific check. /// Note: Shadows PropertyAccessorBase.IsComplexType with Binary-specific check.

View File

@ -116,6 +116,12 @@ public abstract class TypeMetadataBase
/// </summary> /// </summary>
public bool HasComplexProperties { get; protected set; } 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; } public bool IsComplexType { get; init; }
/// <summary> /// <summary>

View File

@ -50,6 +50,14 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary> /// </summary>
internal BinaryPropertySetterBase?[]? CacheMap; 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! #region Typed IdentityMaps - No generic type checks in hot path!
/// <summary> /// <summary>
@ -109,6 +117,10 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
RefIdGetterGuid = (Func<object, Guid>)refIdGetter; RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
break; break;
} }
// Pre-allocate PropertyTypeWrappers — eliminates null/resize checks from hot path
if (metadata.ComplexPropertyCount > 0)
PropertyTypeWrappers = new TypeMetadataWrapper<TMetadata>?[metadata.ComplexPropertyCount];
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -117,6 +129,35 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
return referenceHandling != ReferenceHandlingMode.None && (Metadata.IsIId || referenceHandling == ReferenceHandlingMode.All); 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> /// <summary>
/// Resets tracking state for reuse between serializations. /// Resets tracking state for reuse between serializations.
/// Does not deallocate - just clears for reuse (pool-friendly). /// Does not deallocate - just clears for reuse (pool-friendly).