diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs
index 77ef1df..04309a4 100644
--- a/AyCode.Core.Serializers.Console/Program.cs
+++ b/AyCode.Core.Serializers.Console/Program.cs
@@ -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),
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
index 92b524c..dfe973b 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
@@ -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))
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
index 7da9a30..14372d0 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
@@ -18,8 +18,45 @@ public static partial class AcBinaryDeserializer
#endregion
+ ///
+ /// 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).
+ ///
+ private ref struct PopulateState where TInput : struct, IBinaryInputBase
+ {
+ public BinaryDeserializationContext Context;
+ public object Target;
+ public TypeMetadataWrapper ParentWrapper;
+ public BinaryDeserializeTypeMetadata Metadata;
+ public int NextDepth;
+ public int Depth;
+ public bool IsMergeMode;
+ public bool SkipDefaultWrite;
+ }
+
#region Populate Object Methods
+ ///
+ /// Resolves a property type wrapper using PropertyTypeWrappers cache.
+ /// Falls back to GetWrapper on cache miss and populates the cache.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static TypeMetadataWrapper ResolvePropertyWrapper(
+ TypeMetadataWrapper parentWrapper,
+ int complexPropertyIndex,
+ Type propertyType,
+ BinaryDeserializationContext 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
+ {
+ 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);
}
}
}
///
/// 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.
///
private static void PopulatePropertyWithMarker(
- BinaryDeserializationContext context,
- object target,
+ ref PopulateState 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