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
|
// 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),
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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,8 +254,20 @@ public static partial class AcBinaryDeserializer
|
||||||
TryReadAndSetTypedValue(context, target, propInfo, peekCode))
|
TryReadAndSetTypedValue(context, target, propInfo, peekCode))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var value = ReadValue(context, propInfo.PropertyType, nextDepth);
|
// Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup
|
||||||
propInfo.SetValue(target, value);
|
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)
|
catch (InvalidCastException ex)
|
||||||
{
|
{
|
||||||
|
|
@ -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,27 +507,30 @@ 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)
|
context.ReadByte(); // consume Object marker
|
||||||
existingList.Add(value);
|
newItem = CreateInstance(elementType, elementMetadata);
|
||||||
continue;
|
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);
|
var itemId = idGetter(newItem);
|
||||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||||
{
|
{
|
||||||
// Track this ID as seen in source
|
// Track this ID as seen in source
|
||||||
sourceIds?.Add(itemId);
|
sourceIds?.Add(itemId);
|
||||||
|
|
||||||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
||||||
{
|
{
|
||||||
// Copy properties to existing item (preserves reference)
|
// Copy properties to existing item (preserves reference)
|
||||||
|
|
@ -532,7 +606,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
var arrayCount = (int)context.ReadVarUInt();
|
var arrayCount = (int)context.ReadVarUInt();
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
|
|
||||||
// Track which IDs we see in source (for orphan removal)
|
// Track which IDs we see in source (for orphan removal)
|
||||||
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null
|
||||||
? new HashSet<object>(arrayCount)
|
? new HashSet<object>(arrayCount)
|
||||||
|
|
@ -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)
|
context.ReadByte(); // consume Object marker
|
||||||
existingList.Add(value);
|
newItem = CreateInstance(elementType, elementMetadata);
|
||||||
continue;
|
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);
|
var itemId = idGetter(newItem);
|
||||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,21 +122,22 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
Properties = new BinaryPropertyAccessor[orderedProperties.Length];
|
||||||
var complexCount = 0;
|
var complexCount = 0;
|
||||||
|
|
||||||
for (var i = 0; i < orderedProperties.Length; i++)
|
for (var i = 0; i < orderedProperties.Length; i++)
|
||||||
{
|
{
|
||||||
var accessor = new BinaryPropertyAccessor(orderedProperties[i], type);
|
var accessor = new BinaryPropertyAccessor(orderedProperties[i], type);
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue