diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index 0d4593e..6a5ee75 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -68,7 +68,7 @@ public static class Program } // Profiler mode: warmup only, then exit (for memory profiler analysis) - if (mode == "profiler") + //if (mode == "profiler") { RunProfilerMode(); return; @@ -132,7 +132,7 @@ public static class Program byte[] bytes = AcBinarySerializer.Serialize(order, options); // Warmup (fills caches) - System.Console.WriteLine("Warming up (10 iterations)..."); + System.Console.WriteLine("Warming up (1000 iterations)..."); for (var i = 0; i < 1000; i++) { _ = AcBinarySerializer.Serialize(order, options); @@ -148,7 +148,7 @@ public static class Program for (var i = 0; i < 1000; i++) { _ = AcBinarySerializer.Serialize(order, options); - _ = AcBinaryDeserializer.Deserialize(bytes); + //_ = AcBinaryDeserializer.Deserialize(bytes); } System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)..."); diff --git a/AyCode.Core/Helpers/JsonUtilities.cs b/AyCode.Core/Helpers/JsonUtilities.cs index a189358..62ba7ea 100644 --- a/AyCode.Core/Helpers/JsonUtilities.cs +++ b/AyCode.Core/Helpers/JsonUtilities.cs @@ -86,23 +86,17 @@ public static class JsonUtilities typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(char) }.ToFrozenSet(); - + #endregion #region Type Caches - - private static readonly ConcurrentDictionary IdInfoCache = new(); - private static readonly ConcurrentDictionary CollectionElementCache = new(); - private static readonly ConcurrentDictionary IsPrimitiveCache = new(); - private static readonly ConcurrentDictionary IsCollectionCache = new(); - private static readonly ConcurrentDictionary IsPrimitiveCollectionCache = new(); - private static readonly ConcurrentDictionary JsonIgnoreCache = new(); + private static readonly ConcurrentDictionary> ListFactoryCache = new(); - + #endregion #region UTF8 Buffer Pool - + /// /// Rents a UTF8 byte buffer from the shared pool. /// @@ -368,19 +362,16 @@ public static class JsonUtilities } /// - /// Fast primitive check using type code. + /// Checks if type is primitive, string, or nullable primitive. + /// Delegates to IsPrimitiveOrStringFast with nullable unwrapping. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPrimitiveOrString(Type type) { - return IsPrimitiveCache.GetOrAdd(type, static t => - { - if (t.IsPrimitive || PrimitiveTypes.Contains(t)) return true; - if (t.IsGenericType && t.GetGenericTypeDefinition() == NullableGenericType) - return IsPrimitiveOrString(t.GetGenericArguments()[0]); - if (t.IsEnum) return true; - return false; - }); + if (IsPrimitiveOrStringFast(type)) return true; + if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableGenericType) + return IsPrimitiveOrStringFast(type.GetGenericArguments()[0]); + return false; } /// @@ -402,35 +393,33 @@ public static class JsonUtilities /// /// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.) + /// Only called at metadata/config creation time — not in hot path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsGenericCollectionType(in Type type) { - return IsCollectionCache.GetOrAdd(type, static t => + if (ReferenceEquals(type, StringType) || type.IsPrimitive) return false; + if (type.IsArray) return true; + + if (type.IsGenericType) { - if (ReferenceEquals(t, StringType) || t.IsPrimitive) return false; - if (t.IsArray) return true; - - if (t.IsGenericType) - { - var genericDef = t.GetGenericTypeDefinition(); - if (ReferenceEquals(genericDef, ListGenericType) || - ReferenceEquals(genericDef, IListGenericType) || - genericDef == typeof(ICollection<>) || - ReferenceEquals(genericDef, IEnumerableGenericType) || - ReferenceEquals(genericDef, ObservableCollectionType) || - ReferenceEquals(genericDef, CollectionType)) - return true; - } - - foreach (var iface in t.GetInterfaces()) - { - if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType)) - return true; - } - - return typeof(IEnumerable).IsAssignableFrom(t); - }); + var genericDef = type.GetGenericTypeDefinition(); + if (ReferenceEquals(genericDef, ListGenericType) || + ReferenceEquals(genericDef, IListGenericType) || + genericDef == typeof(ICollection<>) || + ReferenceEquals(genericDef, IEnumerableGenericType) || + ReferenceEquals(genericDef, ObservableCollectionType) || + ReferenceEquals(genericDef, CollectionType)) + return true; + } + + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType)) + return true; + } + + return typeof(IEnumerable).IsAssignableFrom(type); } /// @@ -469,62 +458,57 @@ public static class JsonUtilities /// /// Gets the element type of a collection. + /// Only called at metadata/config creation time — not in hot path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Type? GetCollectionElementType(in Type collectionType) { - return CollectionElementCache.GetOrAdd(collectionType, static type => + if (collectionType.IsArray) + return collectionType.GetElementType(); + + if (collectionType.IsGenericType) { - if (type.IsArray) - return type.GetElementType(); + var genericDef = collectionType.GetGenericTypeDefinition(); + if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) || + genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType)) + return collectionType.GetGenericArguments()[0]; + } + + foreach (var iface in collectionType.GetInterfaces()) + { + if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType)) + return iface.GetGenericArguments()[0]; + } - if (type.IsGenericType) - { - var genericDef = type.GetGenericTypeDefinition(); - if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) || - genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType)) - return type.GetGenericArguments()[0]; - } - - foreach (var iface in type.GetInterfaces()) - { - if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType)) - return iface.GetGenericArguments()[0]; - } - - return typeof(object); - }); + return typeof(object); } /// /// Gets IId info for a type. Returns struct to avoid allocation. + /// Only called at metadata creation time — not in hot path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static IdTypeInfo GetIdInfo(in Type type) { - return IdInfoCache.GetOrAdd(type, static t => + foreach (var iface in type.GetInterfaces()) { - foreach (var iface in t.GetInterfaces()) - { - if (!iface.IsGenericType) continue; - if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue; - var idType = iface.GetGenericArguments()[0]; - // FIXED: IsId should be true if IId interface is found, not idType.IsValueType - return new IdTypeInfo(true, idType); - } - return new IdTypeInfo(false, typeof(int)); - }); + if (!iface.IsGenericType) continue; + if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue; + var idType = iface.GetGenericArguments()[0]; + return new IdTypeInfo(true, idType); + } + return new IdTypeInfo(false, typeof(int)); } /// /// Checks if property has JsonIgnore attribute. + /// Only called at metadata creation time — not in hot path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasJsonIgnoreAttribute(PropertyInfo prop) { - return JsonIgnoreCache.GetOrAdd(prop, static p => - Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) || - Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute))); + return Attribute.IsDefined(prop, typeof(JsonIgnoreAttribute)) || + Attribute.IsDefined(prop, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)); } /// @@ -534,11 +518,6 @@ public static class JsonUtilities public static bool HasToonIgnoreAttribute(PropertyInfo prop) { return false; - //return JsonIgnoreCache.GetOrAdd(prop, static p => Attribute.IsDefined(p, typeof(ToonIgnoreAttribute))); - - return JsonIgnoreCache.GetOrAdd(prop, static p => - Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) || - Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute))); } /// @@ -561,26 +540,24 @@ public static class JsonUtilities /// /// Checks if collection contains primitive elements. + /// Only called at metadata/config creation time — not in hot path. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPrimitiveElementCollection(in Type type) { - return IsPrimitiveCollectionCache.GetOrAdd(type, static t => + if (ReferenceEquals(type, StringType)) return false; + + Type? elementType = null; + if (type.IsArray) + elementType = type.GetElementType(); + else if (type.IsGenericType && typeof(System.Collections.IEnumerable).IsAssignableFrom(type)) { - if (ReferenceEquals(t, StringType)) return false; + var genericArgs = type.GetGenericArguments(); + if (genericArgs.Length == 1) elementType = genericArgs[0]; + } - Type? elementType = null; - if (t.IsArray) - elementType = t.GetElementType(); - else if (t.IsGenericType && typeof(IEnumerable).IsAssignableFrom(t)) - { - var genericArgs = t.GetGenericArguments(); - if (genericArgs.Length == 1) elementType = genericArgs[0]; - } - - if (elementType == null) return false; - return IsPrimitiveOrString(elementType) || elementType.IsEnum; - }); + if (elementType == null) return false; + return IsPrimitiveOrString(elementType) || elementType.IsEnum; } /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index cc94b8e..400ad83 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -71,6 +71,7 @@ public static partial class AcBinarySerializer internal sealed class BinarySerializationContext : SerializationContextBase, IDisposable { private const int MinBufferSize = 512; + private const int BufferHalvingThreshold = 4; // Halve buffer when > _initialBufferSize * this private const int PropertyIndexBufferMaxCache = 512; private const int PropertyStateBufferMaxCache = 512; private const int InitialInternCapacity = 32; @@ -186,7 +187,7 @@ public static partial class AcBinarySerializer // NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt! // Kumulatívan gyűjtjük a benchmark során. - if (_buffer.Length < _initialBufferSize) + if (_buffer.Length < _initialBufferSize || _buffer.Length > _initialBufferSize * BufferHalvingThreshold) { ArrayPool.Shared.Return(_buffer); _buffer = ArrayPool.Shared.Rent(_initialBufferSize); @@ -214,6 +215,14 @@ public static partial class AcBinarySerializer _propertyStateBuffer = null; } + // Halve oversized output buffer (IdentityMap pattern: gradual shrink after spike) + if (_buffer.Length > _initialBufferSize * BufferHalvingThreshold) + { + var nextSize = Math.Max(_buffer.Length / 2, _initialBufferSize); + ArrayPool.Shared.Return(_buffer); + _buffer = ArrayPool.Shared.Rent(nextSize); + } + // Clear wrapper tracking - returns IdentityMap arrays to pool base.Clear(); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs index d9c5775..7cf0410 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializeTypeMetadata.cs @@ -141,7 +141,7 @@ public static partial class AcBinarySerializer // 1. It's IId (can be deduplicated by Id) // 2. It has complex properties (children could be shared) // 3. It's not a primitive/string (could be referenced multiple times) - NeedsReferenceTracking = IsIId || HasComplexProperties || !IsPrimitiveOrStringFast(type); + NeedsReferenceTracking = IsIId || HasComplexProperties || !IsPrimitiveType; // Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false)) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index a4ed916..ab36030 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.CompilerServices; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; @@ -8,7 +9,10 @@ public static partial class AcBinarySerializer /// /// First pass: scans object graph to identify duplicates (strings + objects). /// Only traverses reference properties (complex types + strings). - /// Stops traversing an object after its 2nd occurrence. + /// Stops traversing children after 2nd occurrence of an IId object: + /// - Prevents infinite recursion on circular references + /// - Consistent with write pass which writes ObjectRef (no children) for 2nd occurrence + /// - Strings/objects skipped here are never written anyway (parent is ObjectRef) /// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed). /// private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context) @@ -16,41 +20,43 @@ public static partial class AcBinarySerializer if (!context.HasCaching) return; - ScanValue(value, type, context, 0); - // No AssignCacheIndicesInOrder() needed - CacheIndex assigned inline on 2nd occurrence + var wrapper = context.GetWrapper(type); + ScanValue(value, wrapper, context, 0); } - private static void ScanValue(object? value, Type type, BinarySerializationContext context, int depth) + private static void ScanValue(object? value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) { if (value == null || depth > context.MaxDepth) return; - // String → intern tracking (with length check to match serialize pass) - if (value is string str) - { - if (context.UseStringInterning && context.IsValidForInterningString(str.Length)) - { - context.ScanInternString(str); - } - - return; - } - - // Skip primitives - if (IsPrimitiveOrStringFast(type)) + var metadata = wrapper.Metadata; + + // Skip primitives (pre-computed field, no Type.GetTypeCode() call) + if (metadata.IsPrimitiveType) return; - // Collection → iterate elements - if (value is IEnumerable enumerable) + // Collection → iterate elements using IList fast path (no IEnumerator alloc) + if (metadata.IsCollection) { - var elementType = GetCollectionElementType(type) ?? typeof(object); - if (!IsPrimitiveOrStringFast(elementType) || elementType == typeof(string)) + if (metadata.ElementNeedsScan) { var nextDepth = depth + 1; - foreach (var item in enumerable) + if (value is IList list) { - if (item != null) - ScanValue(item, item.GetType(), context, nextDepth); + for (var i = 0; i < list.Count; i++) + { + var item = list[i]; + if (item != null) + ScanItem(item, context, nextDepth); + } + } + else if (value is IEnumerable enumerable) + { + foreach (var item in enumerable) + { + if (item != null) + ScanItem(item, context, nextDepth); + } } } @@ -58,10 +64,12 @@ public static partial class AcBinarySerializer } // Object → ref tracking + recursive scan - var wrapper = context.GetWrapper(type); - var metadata = wrapper.Metadata; // Reference tracking for IId types (or all types when ReferenceHandling == All) + // 2nd occurrence → skip children because: + // 1. Write pass writes ObjectRef (no children) → strings/objects here are never in output + // 2. Prevents infinite recursion on circular references (A→B→A→...) + // 3. Nested objects reachable from other paths are scanned through those paths if (context.UseTypeReferenceHandling(metadata)) { // Direct tracking call - avoid extra indirection through context @@ -86,7 +94,7 @@ public static partial class AcBinarySerializer } if (!isFirst) - return; // 2nd occurrence → skip children + return; // 2nd occurrence → skip children (symmetric with write pass ObjectRef) } // Recursive scan on reference properties only @@ -105,11 +113,32 @@ public static partial class AcBinarySerializer } else { - // Object property: use generic getter + // Object property: use generic getter, get wrapper for property type var propValue = prop.GetValue(value); if (propValue != null) - ScanValue(propValue, prop.PropertyType, context, nextDepth2); + { + var propWrapper = context.GetWrapper(prop.PropertyType); + ScanValue(propValue, propWrapper, context, nextDepth2); + } } } } + + /// + /// Scans a collection item. Handles string fast path and gets wrapper for the runtime type. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ScanItem(object item, BinarySerializationContext context, int depth) + { + // String fast path — avoid GetWrapper entirely + if (item is string str) + { + if (context.UseStringInterning && context.IsValidForInterningString(str.Length)) + context.ScanInternString(str); + return; + } + + var itemWrapper = context.GetWrapper(item.GetType()); + ScanValue(item, itemWrapper, context, depth); + } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index afa4a8c..5f8775f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -432,15 +432,18 @@ public static partial class AcBinarySerializer return; } + // Get wrapper once — used by both WriteArray and WriteObject + var wrapper = context.GetWrapper(type); + // Handle collections/arrays if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) { - WriteArray(enumerable, type, context, depth); + WriteArray(enumerable, wrapper, context, depth); return; } // Handle complex objects with single-pass reference tracking - WriteObject(value, type, context, depth, isNested: depth > 0); + WriteObject(value, wrapper, context, depth, isNested: depth > 0); } /// @@ -789,9 +792,8 @@ public static partial class AcBinarySerializer #region Complex Type Writers - private static void WriteObject(object value, Type type, BinarySerializationContext context, int depth, bool isNested = false) + private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool isNested = false) { - var wrapper = context.GetWrapper(type); var metadata = wrapper.Metadata; // Wire format: @@ -1269,14 +1271,17 @@ public static partial class AcBinarySerializer /// /// Optimized array writer with specialized paths for primitive arrays. /// - private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth) + private static void WriteArray(IEnumerable enumerable, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth) { context.WriteByte(BinaryTypeCode.Array); var nextDepth = depth + 1; + // Use pre-computed metadata — no GetWrapper or GetCollectionElementType needed + var metadata = wrapper.Metadata; + var elementType = metadata.CollectionElementType; + // Optimized path for primitive arrays - var elementType = GetCollectionElementType(type); - if (elementType != null && type.IsArray) + if (elementType != null && metadata.SourceType.IsArray) { if (TryWritePrimitiveArray(enumerable, elementType, context)) return; diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 7d70b24..a051324 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -1,6 +1,7 @@ using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -123,6 +124,37 @@ public abstract class TypeMetadataBase /// public bool NeedsReferenceTracking { get; protected set; } + /// + /// True if this type is a primitive, string, enum, Guid, DateTime, etc. + /// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path. + /// + public bool IsPrimitiveType { get; } + + /// + /// True if this type implements IEnumerable (excluding string and Dictionary). + /// Pre-computed to replace IsCollectionCache and IsGenericCollectionType() lookups. + /// + public bool IsCollection { get; } + + /// + /// The element type if IsCollection is true, null otherwise. + /// Pre-computed to replace CollectionElementCache lookups. + /// + public Type? CollectionElementType { get; } + + /// + /// True if collection elements need scanning (are complex types or strings). + /// Pre-computed: !IsPrimitiveOrStringFast(elementType) || elementType == typeof(string). + /// Only meaningful when IsCollection is true. + /// + public bool ElementNeedsScan { get; } + + /// + /// True if collection elements are all primitive types (no scanning needed at all). + /// Pre-computed to replace IsPrimitiveCollectionCache. + /// + public bool IsPrimitiveElementCollection { get; } + #endregion /// @@ -158,6 +190,20 @@ public abstract class TypeMetadataBase WritableProperties = allReadable.Where(p => p.CanWrite).ToArray(); IsComplexType = IsComplexType2(type); + IsPrimitiveType = !IsComplexType; + + // Pre-compute collection info — replaces CollectionElementCache / IsCollectionCache lookups + if (!IsPrimitiveType && !ReferenceEquals(type, StringType) && typeof(IEnumerable).IsAssignableFrom(type)) + { + CollectionElementType = GetCollectionElementType(type); + IsCollection = CollectionElementType != null; + if (IsCollection) + { + var elemIsPrimitive = IsPrimitiveOrStringFast(CollectionElementType!); + ElementNeedsScan = !elemIsPrimitive || ReferenceEquals(CollectionElementType, StringType); + IsPrimitiveElementCollection = elemIsPrimitive || CollectionElementType!.IsEnum; + } + } // Cache IId info at construction time - no runtime reflection needed later! var idInfo = GetIdInfo(type);