From 7e7918e0714783ca34a065efeb78dfac8c2cf836 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 14 Feb 2026 01:41:54 +0100 Subject: [PATCH] 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. --- AyCode.Core.Serializers.Console/Program.cs | 16 +- ...erializer.BinaryDeserializeTypeMetadata.cs | 7 +- .../Binaries/AcBinaryDeserializer.Populate.cs | 175 +++++++++++++----- .../Binaries/AcBinaryDeserializer.cs | 10 + ...ySerializer.BinarySerializeTypeMetadata.cs | 15 +- .../Binaries/AcBinarySerializer.ScanPass.cs | 10 +- .../Binaries/AcBinarySerializer.cs | 73 +++++++- .../Binaries/BinaryPropertyAccessorBase.cs | 7 + .../Binaries/BinaryPropertySetterBase.cs | 7 + AyCode.Core/Serializers/TypeMetadataBase.cs | 6 + .../Serializers/TypeMetadataWrapper.cs | 41 ++++ 11 files changed, 296 insertions(+), 71 deletions(-) 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? sourceIds = context.RemoveOrphanedItems && existingById != null ? new HashSet(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)) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 75f68d8..3dc462b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -1116,7 +1116,17 @@ public static partial class AcBinaryDeserializer } var wrapper = context.GetWrapper(targetType); + return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex); + } + + /// + /// Object olvasás with pre-resolved wrapper (eliminates GetWrapper dictionary lookup). + /// + private static object? ReadObjectCoreWithWrapper(BinaryDeserializationContext context, TypeMetadataWrapper 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; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs index ab31d3b..c1a586e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs @@ -75,11 +75,13 @@ public static partial class AcBinarySerializer private BinaryPropertyAccessor[] ComputeReferenceProperties() { var list = new List(); - 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: diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index 394d10c..77e57c3 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -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); } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 9855587..a87e17d 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -497,6 +497,53 @@ public static partial class AcBinarySerializer WriteObject(value, wrapper, context, depth, isNested: depth > 0); } + /// + /// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache). + /// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects. + /// + private static void WriteValueNonPrimitiveWithWrapper(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) + where TOutput : struct, IBinaryOutputBase + { + var type = wrapper.Metadata.SourceType; + + // Nullable 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); + } + /// /// 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. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth) + private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper parentWrapper, BinarySerializationContext 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; } diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs index 1450eb4..5c568f6 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -18,6 +18,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase /// public int PropertyIndex { get; internal set; } = -1; + /// + /// 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. + /// + public int ComplexPropertyIndex { get; internal set; } = -1; + /// /// Cached string intern attribute value for this property. /// null = no attribute (use global StringInterningMode setting) diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs index 8fbc474..415f855 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs @@ -12,6 +12,13 @@ namespace AyCode.Core.Serializers.Binaries; /// public abstract class BinaryPropertySetterBase : PropertySetterBase { + /// + /// 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. + /// + public int ComplexPropertyIndex { get; internal set; } = -1; + /// /// Whether this property is a complex type (not primitive, string, enum, or common value types). /// Note: Shadows PropertyAccessorBase.IsComplexType with Binary-specific check. diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index a051324..45bb4f8 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -116,6 +116,12 @@ public abstract class TypeMetadataBase /// public bool HasComplexProperties { get; protected set; } + /// + /// Number of complex (non-primitive, non-string) properties. + /// Used to allocate PropertyTypeWrappers array on TypeMetadataWrapper. + /// + public int ComplexPropertyCount { get; protected set; } + public bool IsComplexType { get; init; } /// diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index cd40fae..4c9a20e 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -50,6 +50,14 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// internal BinaryPropertySetterBase?[]? CacheMap; + /// + /// 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. + /// + internal TypeMetadataWrapper?[]? PropertyTypeWrappers; + #region Typed IdentityMaps - No generic type checks in hot path! /// @@ -109,6 +117,10 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat RefIdGetterGuid = (Func)refIdGetter; break; } + + // Pre-allocate PropertyTypeWrappers — eliminates null/resize checks from hot path + if (metadata.ComplexPropertyCount > 0) + PropertyTypeWrappers = new TypeMetadataWrapper?[metadata.ComplexPropertyCount]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -117,6 +129,35 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat return referenceHandling != ReferenceHandlingMode.None && (Metadata.IsIId || referenceHandling == ReferenceHandlingMode.All); } + /// + /// Gets a pre-cached wrapper for a complex property type. + /// Returns the cached wrapper if it matches the runtime type, null otherwise. + /// + /// Index from BinaryPropertyAccessorBase.ComplexPropertyIndex (-1 for non-complex) + /// The actual runtime type of the property value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TypeMetadataWrapper? 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; + } + + /// + /// Caches a resolved wrapper for a complex property type. + /// Overwrites any previous value (handles polymorphic types gracefully). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetPropertyTypeWrapper(int complexPropertyIndex, TypeMetadataWrapper wrapper) + { + PropertyTypeWrappers![complexPropertyIndex] = wrapper; + } + /// /// Resets tracking state for reuse between serializations. /// Does not deallocate - just clears for reuse (pool-friendly).