diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs index 2168139..f8f2723 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs @@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinaryDeserializer { - internal sealed class BinaryDeserializeTypeMetadata : BinaryTypeMetadataBase + internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase { /// /// Properties array ordered alphabetically by name for index-based lookup. @@ -19,7 +19,7 @@ public static partial class AcBinaryDeserializer public BinaryDeserializeTypeMetadata(Type type) : base(type) { var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true); - + PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length]; for (var i = 0; i < orderedProperties.Length; i++) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index f1933d6..f92d554 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -41,30 +41,13 @@ public class AcBinaryDeserializationException : Exception /// public static partial class AcBinaryDeserializer { - private static readonly ConcurrentDictionary TypeMetadataCache = new(); private static readonly ConcurrentDictionary TypeConversionCache = new(); - - /// - /// ThreadLocal cache for hot path type metadata lookup. - /// Avoids ConcurrentDictionary overhead for frequently accessed types. - /// - [ThreadStatic] - private static Dictionary? t_deserializeTypeMetadataLocalCache; - + /// /// ThreadLocal cache for type conversion info. /// [ThreadStatic] private static Dictionary? t_typeConversionLocalCache; - - /// - /// Maximum local cache size before clearing. - /// Clear() is preferred over new Dictionary() because: - /// 1. Reuses already allocated internal array (no new allocation) - /// 2. Reduces GC pressure - /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity - /// - private const int MaxLocalCacheSize = 64; // Type dispatch table for fast ReadValue private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth); @@ -769,7 +752,7 @@ public static partial class AcBinaryDeserializer } /// - /// Sima string olvasása - NEM regisztrál az intern táblába. + /// Sima string olvasďż˝sa - NEM regisztrďż˝l az intern tďż˝blďż˝ba. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string ReadPlainString(ref BinaryDeserializationContext context) @@ -780,7 +763,7 @@ public static partial class AcBinaryDeserializer } /// - /// Új internált string olvasása és regisztrálása az intern táblába. + /// ďż˝j internďż˝lt string olvasďż˝sa ďż˝s regisztrďż˝lďż˝sa az intern tďż˝blďż˝ba. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context) @@ -869,9 +852,9 @@ public static partial class AcBinaryDeserializer // Get or create local cache var localCache = t_typeConversionLocalCache ??= new Dictionary(); - + // Clear when full - reuses internal array, better than new Dictionary() - if (localCache.Count >= MaxLocalCacheSize) + if (localCache.Count >= TypeMetadataBase.MaxLocalCacheSize) { localCache.Clear(); } @@ -1250,14 +1233,14 @@ public static partial class AcBinaryDeserializer context.Skip(16); return; case BinaryTypeCode.String: - // Sima string - nem regisztrálunk + // Sima string - nem regisztrďż˝lunk SkipPlainString(ref context); return; case BinaryTypeCode.StringInterned: context.ReadVarUInt(); return; case BinaryTypeCode.StringInternNew: - // Új internált string - regisztrálni kell még skip esetén is + // ďż˝j internďż˝lt string - regisztrďż˝lni kell mďż˝g skip esetďż˝n is SkipAndRegisterInternedString(ref context); return; case BinaryTypeCode.ByteArray: @@ -1285,7 +1268,7 @@ public static partial class AcBinaryDeserializer } /// - /// Sima string kihagyása - NEM regisztrál. + /// Sima string kihagyďż˝sa - NEM regisztrďż˝l. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SkipPlainString(ref BinaryDeserializationContext context) @@ -1298,7 +1281,7 @@ public static partial class AcBinaryDeserializer } /// - /// Új internált string kihagyása - DE regisztrálni kell! + /// ďż˝j internďż˝lt string kihagyďż˝sa - DE regisztrďż˝lni kell! /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context) @@ -1369,38 +1352,11 @@ public static partial class AcBinaryDeserializer /// /// Gets type metadata with ThreadLocal caching for hot path optimization. + /// Uses built-in cache from BinaryDeserializeTypeMetadata base class (zero ref parameter overhead). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) - { - // Fast path: check ThreadLocal cache first - var localCache = t_deserializeTypeMetadataLocalCache; - if (localCache != null && localCache.TryGetValue(type, out var cached)) - { - return cached; - } - - // Slow path: get from global cache and populate local cache - return GetTypeMetadataSlow(type); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static BinaryDeserializeTypeMetadata GetTypeMetadataSlow(Type type) - { - var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); - - // Get or create local cache - var localCache = t_deserializeTypeMetadataLocalCache ??= new Dictionary(); - - // Clear when full - reuses internal array, better than new Dictionary() - if (localCache.Count >= MaxLocalCacheSize) - { - localCache.Clear(); - } - - localCache[type] = metadata; - return metadata; - } + => BinaryDeserializeTypeMetadata.GetOrCreateMetadata(type, static t => new BinaryDeserializeTypeMetadata(t)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs index 1c5ef2a..8f80ce7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs @@ -7,7 +7,7 @@ namespace AyCode.Core.Serializers.Binaries; public static partial class AcBinarySerializer { - internal sealed class BinaryTypeMetadata : BinaryTypeMetadataBase + internal sealed class BinaryTypeMetadata : TypeMetadataBase { public BinaryPropertyAccessor[] Properties { get; } @@ -17,7 +17,7 @@ public static partial class AcBinarySerializer // This ensures read-only properties (like computed properties) are excluded // and property indices are consistent between serialization and deserialization var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true); - + Properties = new BinaryPropertyAccessor[orderedProperties.Length]; for (var i = 0; i < orderedProperties.Length; i++) { diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 6bfc1df..27ac81c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -25,23 +25,6 @@ namespace AyCode.Core.Serializers.Binaries; /// public static partial class AcBinarySerializer { - private static readonly ConcurrentDictionary TypeMetadataCache = new(); - - /// - /// ThreadLocal cache for hot path type metadata lookup. - /// Avoids ConcurrentDictionary overhead for frequently accessed types. - /// - [ThreadStatic] - private static Dictionary? t_typeMetadataLocalCache; - - /// - /// Maximum local cache size before clearing. - /// Clear() is preferred over new Dictionary() because: - /// 1. Reuses already allocated internal array (no new allocation) - /// 2. Reduces GC pressure - /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity - /// - private const int MaxLocalCacheSize = 64; // Pre-computed UTF8 encoder for string operations private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); @@ -1096,39 +1079,11 @@ public static partial class AcBinarySerializer /// /// Gets type metadata with ThreadLocal caching for hot path optimization. - /// Falls back to ConcurrentDictionary for cache misses. + /// Uses built-in cache from BinaryTypeMetadata base class (zero ref parameter overhead). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BinaryTypeMetadata GetTypeMetadata(Type type) - { - // Fast path: check ThreadLocal cache first - var localCache = t_typeMetadataLocalCache; - if (localCache != null && localCache.TryGetValue(type, out var cached)) - { - return cached; - } - - // Slow path: get from global cache and populate local cache - return GetTypeMetadataSlow(type); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static BinaryTypeMetadata GetTypeMetadataSlow(Type type) - { - var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t)); - - // Get or create local cache - var localCache = t_typeMetadataLocalCache ??= new Dictionary(); - - // Clear when full - reuses internal array, better than new Dictionary() - if (localCache.Count >= MaxLocalCacheSize) - { - localCache.Clear(); - } - - localCache[type] = metadata; - return metadata; - } + => BinaryTypeMetadata.GetOrCreateMetadata(type, static t => new BinaryTypeMetadata(t)); // Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs deleted file mode 100644 index 2255217..0000000 --- a/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace AyCode.Core.Serializers.Binaries; - -/// -/// Base class for binary serializer/deserializer type metadata. -/// Extends DeserializeTypeMetadataBase (which provides IId caching). -/// Properties are ordered alphabetically by name for deterministic serialization across platforms. -/// -public abstract class BinaryTypeMetadataBase : DeserializeTypeMetadataBase -{ - protected BinaryTypeMetadataBase(Type type) : base(type) - { - // IId info is now cached in DeserializeTypeMetadataBase - } -} diff --git a/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs b/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs index 26b6dee..6cb9e8f 100644 --- a/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs +++ b/AyCode.Core/Serializers/DeserializeTypeMetadataBase.cs @@ -7,20 +7,22 @@ namespace AyCode.Core.Serializers; /// Base class for deserializer type metadata. /// Extends TypeMetadataBase with IId detection for efficient chain/merge operations. /// Used by both JSON and Binary deserializers. +/// Generic version provides built-in ThreadLocal caching with zero ref parameter overhead. /// -public abstract class DeserializeTypeMetadataBase : TypeMetadataBase +public abstract class DeserializeTypeMetadataBase : TypeMetadataBase + where TMetadata : DeserializeTypeMetadataBase { /// /// Whether this type implements IId interface. /// Cached at metadata creation time to avoid runtime reflection. /// public bool IsIId { get; } - + /// /// The Id property type if IsIId is true, null otherwise. /// public Type? IdType { get; } - + /// /// Compiled getter for the Id property (if IsIId is true). /// Pre-compiled delegate avoids reflection overhead during deserialization. @@ -33,7 +35,7 @@ public abstract class DeserializeTypeMetadataBase : TypeMetadataBase var idInfo = GetIdInfo(type); IsIId = idInfo.IsId; IdType = idInfo.IdType; - + if (IsIId) { var idProp = type.GetProperty("Id"); diff --git a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs index fdfc387..076c76d 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.JsonDeserializeTypeMetadata.cs @@ -9,39 +9,37 @@ namespace AyCode.Core.Serializers.Jsons; public static partial class AcJsonDeserializer { - private static readonly ConcurrentDictionary TypeMetadataCache = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static JsonDeserializeTypeMetadata GetTypeMetadata(in Type type) - => TypeMetadataCache.GetOrAdd(type, static t => new JsonDeserializeTypeMetadata(t)); + => JsonDeserializeTypeMetadata.GetOrCreateMetadata(type, static t => new JsonDeserializeTypeMetadata(t)); /// /// JSON deserialization type metadata. /// Extends DeserializeTypeMetadataBase which provides cached IId info. /// - private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase + private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase { public FrozenDictionary PropertySettersFrozen { get; } public PropertySetterInfo[] PropertiesArray { get; } // Array for fast UTF8 linear scan (small objects) public JsonDeserializeTypeMetadata(Type type) : base(type) { - var props = GetSerializableProperties(type, requiresRead: true, requiresWrite: true).ToList(); + var props = GetSerializableProperties(type, requiresRead: true, requiresWrite: true); - var propertySetters = new Dictionary(props.Count, StringComparer.OrdinalIgnoreCase); - var propsArray = new PropertySetterInfo[props.Count]; + var propertySetters = new Dictionary(props.Length, StringComparer.OrdinalIgnoreCase); + var propsArray = new PropertySetterInfo[props.Length]; var index = 0; - + foreach (var prop in props) { var propInfo = new PropertySetterInfo(prop, type); propertySetters[prop.Name] = propInfo; propsArray[index++] = propInfo; } - + PropertiesArray = propsArray; PropertySettersFrozen = propertySetters.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - + // IId info (IsIId, IdType, IdGetter) is now cached in DeserializeTypeMetadataBase } diff --git a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs index 2e1cc7f..0a1beae 100644 --- a/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs +++ b/AyCode.Core/Serializers/Jsons/AcJsonSerializer.JsonTypeMetadata.cs @@ -7,13 +7,11 @@ namespace AyCode.Core.Serializers.Jsons; public static partial class AcJsonSerializer { - private static readonly ConcurrentDictionary TypeMetadataCache = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static JsonTypeMetadata GetTypeMetadata(in Type type) - => TypeMetadataCache.GetOrAdd(type, static t => new JsonTypeMetadata(t)); + => JsonTypeMetadata.GetOrCreateMetadata(type, static t => new JsonTypeMetadata(t)); - private sealed class JsonTypeMetadata : TypeMetadataBase + private sealed class JsonTypeMetadata : TypeMetadataBase { public PropertyAccessor[] Properties { get; } diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index e624144..81ebe6b 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Reflection; +using System.Runtime.CompilerServices; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers; @@ -10,6 +11,22 @@ namespace AyCode.Core.Serializers; /// public abstract class TypeMetadataBase { + /// + /// Global shared cache for all metadata types. + /// Key: (Type sourceType, Type metadataType) - ensures each type can have multiple metadata representations + /// Value: TypeMetadataBase instance (BinaryTypeMetadata, JsonTypeMetadata, etc.) + /// This single cache is shared across Binary/JSON Serializers/Deserializers. + /// + protected static readonly ConcurrentDictionary<(Type, Type), TypeMetadataBase> GlobalMetadataCache = new(); + + /// + /// Maximum ThreadLocal cache size before clearing. + /// Set to 256 because if a system uses 256+ different types, they're likely all frequently accessed. + /// Minimal memory overhead (256 dictionary entries ≈ 8KB) vs significant lookup performance benefit. + /// Clear() reuses internal array - no new allocation, just resets count. + /// + internal const int MaxLocalCacheSize = 1024; + /// /// Cache for serializable properties per type. /// Key: (Type, requiresWrite) - since requiresRead is always true in practice. @@ -51,24 +68,14 @@ public abstract class TypeMetadataBase { var (t, needsWrite) = key; - // Build inheritance hierarchy (base -> derived) - var hierarchy = new Stack(); - var currentType = t; - while (currentType != null && currentType != typeof(object)) - { - hierarchy.Push(currentType); - currentType = currentType.BaseType; - } - - // Collect properties level by level (base first) + // Collect properties from inheritance hierarchy (derived -> base order) + // Then sort alphabetically - ensures consistent ordering for serialization/deserialization var allProperties = new List(); - while (hierarchy.Count > 0) + for (var currentType = t; currentType != null && currentType != typeof(object); currentType = currentType.BaseType) { - var levelType = hierarchy.Pop(); - // Get properties declared at this level only - var levelProperties = levelType + var levelProperties = currentType .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.CanRead && (!needsWrite || p.CanWrite) && @@ -83,3 +90,60 @@ public abstract class TypeMetadataBase }); } } + +/// +/// Generic base class for type metadata with built-in ThreadLocal caching. +/// Provides better performance than TypeMetadataBase by eliminating ref parameter overhead. +/// Each TMetadata type gets its own ThreadStatic cache instance automatically. +/// +/// The concrete metadata type (must inherit from this class). +public abstract class TypeMetadataBase : TypeMetadataBase + where TMetadata : TypeMetadataBase +{ + /// + /// ThreadLocal cache for this specific metadata type. + /// Each TMetadata type gets its own static cache due to generic specialization. + /// + [ThreadStatic] + private static Dictionary? t_localCache; + + protected TypeMetadataBase(Type type) : base(type) + { + } + + /// + /// Gets or creates metadata for the specified type using two-level cache: + /// 1. ThreadLocal cache (fast path, no lock, no ref parameter) + /// 2. Global cache (slow path, ConcurrentDictionary with lock) + /// + /// The type to get metadata for. + /// Factory function to create metadata if not cached. + /// Cached or newly created metadata instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static TMetadata GetOrCreateMetadata(Type sourceType, Func factory) + { + // Fast path: check ThreadLocal cache first (no ref parameter overhead!) + if (t_localCache != null && t_localCache.TryGetValue(sourceType, out var cached)) + return cached; + + // Slow path: get from global cache and populate local cache + return GetOrCreateMetadataSlow(sourceType, factory); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static TMetadata GetOrCreateMetadataSlow(Type sourceType, Func factory) + { + // Get or create from global cache + var key = (sourceType, typeof(TMetadata)); + var metadata = (TMetadata)GlobalMetadataCache.GetOrAdd(key, _ => factory(sourceType)); + + // Populate ThreadLocal cache + t_localCache ??= new Dictionary(); + + if (t_localCache.Count >= MaxLocalCacheSize) + t_localCache.Clear(); + + t_localCache[sourceType] = metadata; + return metadata; + } +}