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).