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:
parent
a0a6ac8ef4
commit
7e7918e071
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue