Refactor type metadata caching for serializers/deserializers
Major overhaul of type metadata caching: - Introduced generic TypeMetadataBase<T> with built-in ThreadLocal and global caching, replacing all per-class static caches. - Updated all serializer/deserializer metadata classes to use the new base, unifying and simplifying cache logic. - Increased ThreadLocal cache size for better performance with many types. - Standardized property collection order and improved property array usage. - Updated documentation and comments to reflect new caching strategy. These changes improve performance, scalability, and maintainability while reducing code duplication.
This commit is contained in:
parent
60ca154c6f
commit
9ad84ec21e
|
|
@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
|
||||
public static partial class AcBinaryDeserializer
|
||||
{
|
||||
internal sealed class BinaryDeserializeTypeMetadata : BinaryTypeMetadataBase
|
||||
internal sealed class BinaryDeserializeTypeMetadata : DeserializeTypeMetadataBase<BinaryDeserializeTypeMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// Properties array ordered alphabetically by name for index-based lookup.
|
||||
|
|
|
|||
|
|
@ -41,31 +41,14 @@ public class AcBinaryDeserializationException : Exception
|
|||
/// </summary>
|
||||
public static partial class AcBinaryDeserializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, BinaryDeserializeTypeMetadata> TypeMetadataCache = new();
|
||||
private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// ThreadLocal cache for hot path type metadata lookup.
|
||||
/// Avoids ConcurrentDictionary overhead for frequently accessed types.
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, BinaryDeserializeTypeMetadata>? t_deserializeTypeMetadataLocalCache;
|
||||
|
||||
/// <summary>
|
||||
/// ThreadLocal cache for type conversion info.
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sima string olvasása - NEM regisztrál az intern táblába.
|
||||
/// Sima string olvas<EFBFBD>sa - NEM regisztr<74>l az intern t<>bl<62>ba.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string ReadPlainString(ref BinaryDeserializationContext context)
|
||||
|
|
@ -780,7 +763,7 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Új internált string olvasása és regisztrálása az intern táblába.
|
||||
/// <EFBFBD>j intern<72>lt string olvas<61>sa <20>s regisztr<74>l<EFBFBD>sa az intern t<>bl<62>ba.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||
|
|
@ -871,7 +854,7 @@ public static partial class AcBinaryDeserializer
|
|||
var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();
|
||||
|
||||
// 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<EFBFBD>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
|
||||
// <EFBFBD>j intern<72>lt string - regisztr<74>lni kell m<>g skip eset<65>n is
|
||||
SkipAndRegisterInternedString(ref context);
|
||||
return;
|
||||
case BinaryTypeCode.ByteArray:
|
||||
|
|
@ -1285,7 +1268,7 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sima string kihagyása - NEM regisztrál.
|
||||
/// Sima string kihagy<EFBFBD>sa - NEM regisztr<74>l.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SkipPlainString(ref BinaryDeserializationContext context)
|
||||
|
|
@ -1298,7 +1281,7 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Új internált string kihagyása - DE regisztrálni kell!
|
||||
/// <EFBFBD>j intern<72>lt string kihagy<67>sa - DE regisztr<74>lni kell!
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||
|
|
@ -1369,38 +1352,11 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
/// <summary>
|
||||
/// Gets type metadata with ThreadLocal caching for hot path optimization.
|
||||
/// Uses built-in cache from BinaryDeserializeTypeMetadata base class (zero ref parameter overhead).
|
||||
/// </summary>
|
||||
[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<Type, BinaryDeserializeTypeMetadata>();
|
||||
|
||||
// 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)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
|
||||
public static partial class AcBinarySerializer
|
||||
{
|
||||
internal sealed class BinaryTypeMetadata : BinaryTypeMetadataBase
|
||||
internal sealed class BinaryTypeMetadata : TypeMetadataBase<BinaryTypeMetadata>
|
||||
{
|
||||
public BinaryPropertyAccessor[] Properties { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -25,23 +25,6 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
/// </summary>
|
||||
public static partial class AcBinarySerializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, BinaryTypeMetadata> TypeMetadataCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// ThreadLocal cache for hot path type metadata lookup.
|
||||
/// Avoids ConcurrentDictionary overhead for frequently accessed types.
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, BinaryTypeMetadata>? t_typeMetadataLocalCache;
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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
|
|||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[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<Type, BinaryTypeMetadata>();
|
||||
|
||||
// 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public abstract class BinaryTypeMetadataBase : DeserializeTypeMetadataBase
|
||||
{
|
||||
protected BinaryTypeMetadataBase(Type type) : base(type)
|
||||
{
|
||||
// IId info is now cached in DeserializeTypeMetadataBase
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ 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.
|
||||
/// </summary>
|
||||
public abstract class DeserializeTypeMetadataBase : TypeMetadataBase
|
||||
public abstract class DeserializeTypeMetadataBase<TMetadata> : TypeMetadataBase<TMetadata>
|
||||
where TMetadata : DeserializeTypeMetadataBase<TMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this type implements IId interface.
|
||||
|
|
|
|||
|
|
@ -9,27 +9,25 @@ namespace AyCode.Core.Serializers.Jsons;
|
|||
|
||||
public static partial class AcJsonDeserializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, JsonDeserializeTypeMetadata> 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));
|
||||
|
||||
/// <summary>
|
||||
/// JSON deserialization type metadata.
|
||||
/// Extends DeserializeTypeMetadataBase which provides cached IId info.
|
||||
/// </summary>
|
||||
private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase
|
||||
private sealed class JsonDeserializeTypeMetadata : DeserializeTypeMetadataBase<JsonDeserializeTypeMetadata>
|
||||
{
|
||||
public FrozenDictionary<string, PropertySetterInfo> 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<string, PropertySetterInfo>(props.Count, StringComparer.OrdinalIgnoreCase);
|
||||
var propsArray = new PropertySetterInfo[props.Count];
|
||||
var propertySetters = new Dictionary<string, PropertySetterInfo>(props.Length, StringComparer.OrdinalIgnoreCase);
|
||||
var propsArray = new PropertySetterInfo[props.Length];
|
||||
var index = 0;
|
||||
|
||||
foreach (var prop in props)
|
||||
|
|
|
|||
|
|
@ -7,13 +7,11 @@ namespace AyCode.Core.Serializers.Jsons;
|
|||
|
||||
public static partial class AcJsonSerializer
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, JsonTypeMetadata> 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<JsonTypeMetadata>
|
||||
{
|
||||
public PropertyAccessor[] Properties { get; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
/// </summary>
|
||||
public abstract class TypeMetadataBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
protected static readonly ConcurrentDictionary<(Type, Type), TypeMetadataBase> GlobalMetadataCache = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal const int MaxLocalCacheSize = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// 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<Type>();
|
||||
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<PropertyInfo>();
|
||||
|
||||
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
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="TMetadata">The concrete metadata type (must inherit from this class).</typeparam>
|
||||
public abstract class TypeMetadataBase<TMetadata> : TypeMetadataBase
|
||||
where TMetadata : TypeMetadataBase<TMetadata>
|
||||
{
|
||||
/// <summary>
|
||||
/// ThreadLocal cache for this specific metadata type.
|
||||
/// Each TMetadata type gets its own static cache due to generic specialization.
|
||||
/// </summary>
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, TMetadata>? t_localCache;
|
||||
|
||||
protected TypeMetadataBase(Type type) : base(type)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
/// <param name="sourceType">The type to get metadata for.</param>
|
||||
/// <param name="factory">Factory function to create metadata if not cached.</param>
|
||||
/// <returns>Cached or newly created metadata instance.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static TMetadata GetOrCreateMetadata(Type sourceType, Func<Type, TMetadata> 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<Type, TMetadata> 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<Type, TMetadata>();
|
||||
|
||||
if (t_localCache.Count >= MaxLocalCacheSize)
|
||||
t_localCache.Clear();
|
||||
|
||||
t_localCache[sourceType] = metadata;
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue