Optimize serialization: precompute type metadata, remove caches

Refactored serialization for performance:
- Precompute type metadata (primitive, collection, element info) in TypeMetadataBase
- Remove runtime type caches from JsonUtilities
- Rewrite primitive/collection checks to use direct logic or metadata
- Update scan pass and serialization hot path to use wrappers/metadata
- Improve buffer management (halve oversized buffers)
- Increase profiler warmup iterations, comment out deserialization in hot path
- Clean up code and clarify documentation/comments

Reduces runtime overhead and memory usage, streamlines hot path execution.
This commit is contained in:
Loretta 2026-02-08 08:13:34 +01:00
parent 5a174ced4c
commit b37d873792
7 changed files with 202 additions and 136 deletions

View File

@ -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<TestOrder>(bytes);
//_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
}
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");

View File

@ -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<Type, IdTypeInfo> IdInfoCache = new();
private static readonly ConcurrentDictionary<Type, Type?> CollectionElementCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsCollectionCache = new();
private static readonly ConcurrentDictionary<Type, bool> IsPrimitiveCollectionCache = new();
private static readonly ConcurrentDictionary<PropertyInfo, bool> JsonIgnoreCache = new();
private static readonly ConcurrentDictionary<Type, Func<int, IList>> ListFactoryCache = new();
#endregion
#region UTF8 Buffer Pool
/// <summary>
/// Rents a UTF8 byte buffer from the shared pool.
/// </summary>
@ -368,19 +362,16 @@ public static class JsonUtilities
}
/// <summary>
/// Fast primitive check using type code.
/// Checks if type is primitive, string, or nullable primitive.
/// Delegates to IsPrimitiveOrStringFast with nullable unwrapping.
/// </summary>
[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;
}
/// <summary>
@ -402,35 +393,33 @@ public static class JsonUtilities
/// <summary>
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
/// Only called at metadata/config creation time — not in hot path.
/// </summary>
[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);
}
/// <summary>
@ -469,62 +458,57 @@ public static class JsonUtilities
/// <summary>
/// Gets the element type of a collection.
/// Only called at metadata/config creation time — not in hot path.
/// </summary>
[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);
}
/// <summary>
/// Gets IId info for a type. Returns struct to avoid allocation.
/// Only called at metadata creation time — not in hot path.
/// </summary>
[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<T> 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));
}
/// <summary>
/// Checks if property has JsonIgnore attribute.
/// Only called at metadata creation time — not in hot path.
/// </summary>
[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));
}
/// <summary>
@ -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)));
}
/// <summary>
@ -561,26 +540,24 @@ public static class JsonUtilities
/// <summary>
/// Checks if collection contains primitive elements.
/// Only called at metadata/config creation time — not in hot path.
/// </summary>
[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;
}
/// <summary>

View File

@ -71,6 +71,7 @@ public static partial class AcBinarySerializer
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, 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<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.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<byte>.Shared.Return(_buffer);
_buffer = ArrayPool<byte>.Shared.Rent(nextSize);
}
// Clear wrapper tracking - returns IdentityMap arrays to pool
base.Clear();
}

View File

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

View File

@ -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
/// <summary>
/// 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).
/// </summary>
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<BinarySerializeTypeMetadata> 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);
}
}
}
}
/// <summary>
/// Scans a collection item. Handles string fast path and gets wrapper for the runtime type.
/// </summary>
[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);
}
}

View File

@ -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);
}
/// <summary>
@ -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<BinarySerializeTypeMetadata> 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
/// <summary>
/// Optimized array writer with specialized paths for primitive arrays.
/// </summary>
private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth)
private static void WriteArray(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> 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;

View File

@ -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
/// </summary>
public bool NeedsReferenceTracking { get; protected set; }
/// <summary>
/// True if this type is a primitive, string, enum, Guid, DateTime, etc.
/// Pre-computed to avoid IsPrimitiveOrStringFast() calls in hot path.
/// </summary>
public bool IsPrimitiveType { get; }
/// <summary>
/// True if this type implements IEnumerable (excluding string and Dictionary).
/// Pre-computed to replace IsCollectionCache and IsGenericCollectionType() lookups.
/// </summary>
public bool IsCollection { get; }
/// <summary>
/// The element type if IsCollection is true, null otherwise.
/// Pre-computed to replace CollectionElementCache lookups.
/// </summary>
public Type? CollectionElementType { get; }
/// <summary>
/// True if collection elements need scanning (are complex types or strings).
/// Pre-computed: !IsPrimitiveOrStringFast(elementType) || elementType == typeof(string).
/// Only meaningful when IsCollection is true.
/// </summary>
public bool ElementNeedsScan { get; }
/// <summary>
/// True if collection elements are all primitive types (no scanning needed at all).
/// Pre-computed to replace IsPrimitiveCollectionCache.
/// </summary>
public bool IsPrimitiveElementCollection { get; }
#endregion
/// <summary>
@ -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);