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:
parent
5a174ced4c
commit
b37d873792
|
|
@ -68,7 +68,7 @@ public static class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profiler mode: warmup only, then exit (for memory profiler analysis)
|
// Profiler mode: warmup only, then exit (for memory profiler analysis)
|
||||||
if (mode == "profiler")
|
//if (mode == "profiler")
|
||||||
{
|
{
|
||||||
RunProfilerMode();
|
RunProfilerMode();
|
||||||
return;
|
return;
|
||||||
|
|
@ -132,7 +132,7 @@ public static class Program
|
||||||
|
|
||||||
byte[] bytes = AcBinarySerializer.Serialize(order, options);
|
byte[] bytes = AcBinarySerializer.Serialize(order, options);
|
||||||
// Warmup (fills caches)
|
// Warmup (fills caches)
|
||||||
System.Console.WriteLine("Warming up (10 iterations)...");
|
System.Console.WriteLine("Warming up (1000 iterations)...");
|
||||||
for (var i = 0; i < 1000; i++)
|
for (var i = 0; i < 1000; i++)
|
||||||
{
|
{
|
||||||
_ = AcBinarySerializer.Serialize(order, options);
|
_ = AcBinarySerializer.Serialize(order, options);
|
||||||
|
|
@ -148,7 +148,7 @@ public static class Program
|
||||||
for (var i = 0; i < 1000; i++)
|
for (var i = 0; i < 1000; i++)
|
||||||
{
|
{
|
||||||
_ = AcBinarySerializer.Serialize(order, options);
|
_ = AcBinarySerializer.Serialize(order, options);
|
||||||
_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
//_ = AcBinaryDeserializer.Deserialize<TestOrder>(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");
|
System.Console.WriteLine("Running hot path deserialization (1000 iterations for profiling)...");
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,6 @@ public static class JsonUtilities
|
||||||
|
|
||||||
#region Type Caches
|
#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();
|
private static readonly ConcurrentDictionary<Type, Func<int, IList>> ListFactoryCache = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -368,19 +362,16 @@ public static class JsonUtilities
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fast primitive check using type code.
|
/// Checks if type is primitive, string, or nullable primitive.
|
||||||
|
/// Delegates to IsPrimitiveOrStringFast with nullable unwrapping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool IsPrimitiveOrString(Type type)
|
public static bool IsPrimitiveOrString(Type type)
|
||||||
{
|
{
|
||||||
return IsPrimitiveCache.GetOrAdd(type, static t =>
|
if (IsPrimitiveOrStringFast(type)) return true;
|
||||||
{
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == NullableGenericType)
|
||||||
if (t.IsPrimitive || PrimitiveTypes.Contains(t)) return true;
|
return IsPrimitiveOrStringFast(type.GetGenericArguments()[0]);
|
||||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == NullableGenericType)
|
return false;
|
||||||
return IsPrimitiveOrString(t.GetGenericArguments()[0]);
|
|
||||||
if (t.IsEnum) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -402,35 +393,33 @@ public static class JsonUtilities
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
|
/// Checks if type is a generic collection type (List, IList, ObservableCollection, etc.)
|
||||||
|
/// Only called at metadata/config creation time — not in hot path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool IsGenericCollectionType(in Type type)
|
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;
|
var genericDef = type.GetGenericTypeDefinition();
|
||||||
if (t.IsArray) return true;
|
if (ReferenceEquals(genericDef, ListGenericType) ||
|
||||||
|
ReferenceEquals(genericDef, IListGenericType) ||
|
||||||
|
genericDef == typeof(ICollection<>) ||
|
||||||
|
ReferenceEquals(genericDef, IEnumerableGenericType) ||
|
||||||
|
ReferenceEquals(genericDef, ObservableCollectionType) ||
|
||||||
|
ReferenceEquals(genericDef, CollectionType))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (t.IsGenericType)
|
foreach (var iface in type.GetInterfaces())
|
||||||
{
|
{
|
||||||
var genericDef = t.GetGenericTypeDefinition();
|
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
||||||
if (ReferenceEquals(genericDef, ListGenericType) ||
|
return true;
|
||||||
ReferenceEquals(genericDef, IListGenericType) ||
|
}
|
||||||
genericDef == typeof(ICollection<>) ||
|
|
||||||
ReferenceEquals(genericDef, IEnumerableGenericType) ||
|
|
||||||
ReferenceEquals(genericDef, ObservableCollectionType) ||
|
|
||||||
ReferenceEquals(genericDef, CollectionType))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var iface in t.GetInterfaces())
|
return typeof(IEnumerable).IsAssignableFrom(type);
|
||||||
{
|
|
||||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof(IEnumerable).IsAssignableFrom(t);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -469,62 +458,57 @@ public static class JsonUtilities
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the element type of a collection.
|
/// Gets the element type of a collection.
|
||||||
|
/// Only called at metadata/config creation time — not in hot path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Type? GetCollectionElementType(in Type collectionType)
|
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)
|
var genericDef = collectionType.GetGenericTypeDefinition();
|
||||||
return type.GetElementType();
|
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
|
||||||
|
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
|
||||||
|
return collectionType.GetGenericArguments()[0];
|
||||||
|
}
|
||||||
|
|
||||||
if (type.IsGenericType)
|
foreach (var iface in collectionType.GetInterfaces())
|
||||||
{
|
{
|
||||||
var genericDef = type.GetGenericTypeDefinition();
|
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
||||||
if (ReferenceEquals(genericDef, ListGenericType) || ReferenceEquals(genericDef, IListGenericType) ||
|
return iface.GetGenericArguments()[0];
|
||||||
genericDef == typeof(ICollection<>) || ReferenceEquals(genericDef, IEnumerableGenericType))
|
}
|
||||||
return type.GetGenericArguments()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var iface in type.GetInterfaces())
|
return typeof(object);
|
||||||
{
|
|
||||||
if (iface.IsGenericType && ReferenceEquals(iface.GetGenericTypeDefinition(), IListGenericType))
|
|
||||||
return iface.GetGenericArguments()[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return typeof(object);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets IId info for a type. Returns struct to avoid allocation.
|
/// Gets IId info for a type. Returns struct to avoid allocation.
|
||||||
|
/// Only called at metadata creation time — not in hot path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static IdTypeInfo GetIdInfo(in Type type)
|
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;
|
||||||
if (!iface.IsGenericType) continue;
|
var idType = iface.GetGenericArguments()[0];
|
||||||
if (!ReferenceEquals(iface.GetGenericTypeDefinition(), IIdGenericType)) continue;
|
return new IdTypeInfo(true, idType);
|
||||||
var idType = iface.GetGenericArguments()[0];
|
}
|
||||||
// FIXED: IsId should be true if IId<T> interface is found, not idType.IsValueType
|
return new IdTypeInfo(false, typeof(int));
|
||||||
return new IdTypeInfo(true, idType);
|
|
||||||
}
|
|
||||||
return new IdTypeInfo(false, typeof(int));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if property has JsonIgnore attribute.
|
/// Checks if property has JsonIgnore attribute.
|
||||||
|
/// Only called at metadata creation time — not in hot path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool HasJsonIgnoreAttribute(PropertyInfo prop)
|
public static bool HasJsonIgnoreAttribute(PropertyInfo prop)
|
||||||
{
|
{
|
||||||
return JsonIgnoreCache.GetOrAdd(prop, static p =>
|
return Attribute.IsDefined(prop, typeof(JsonIgnoreAttribute)) ||
|
||||||
Attribute.IsDefined(p, typeof(JsonIgnoreAttribute)) ||
|
Attribute.IsDefined(prop, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute));
|
||||||
Attribute.IsDefined(p, typeof(System.Text.Json.Serialization.JsonIgnoreAttribute)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -534,11 +518,6 @@ public static class JsonUtilities
|
||||||
public static bool HasToonIgnoreAttribute(PropertyInfo prop)
|
public static bool HasToonIgnoreAttribute(PropertyInfo prop)
|
||||||
{
|
{
|
||||||
return false;
|
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>
|
/// <summary>
|
||||||
|
|
@ -561,26 +540,24 @@ public static class JsonUtilities
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if collection contains primitive elements.
|
/// Checks if collection contains primitive elements.
|
||||||
|
/// Only called at metadata/config creation time — not in hot path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool IsPrimitiveElementCollection(in Type type)
|
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 (elementType == null) return false;
|
||||||
if (t.IsArray)
|
return IsPrimitiveOrString(elementType) || elementType.IsEnum;
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ public static partial class AcBinarySerializer
|
||||||
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
|
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
|
||||||
{
|
{
|
||||||
private const int MinBufferSize = 512;
|
private const int MinBufferSize = 512;
|
||||||
|
private const int BufferHalvingThreshold = 4; // Halve buffer when > _initialBufferSize * this
|
||||||
private const int PropertyIndexBufferMaxCache = 512;
|
private const int PropertyIndexBufferMaxCache = 512;
|
||||||
private const int PropertyStateBufferMaxCache = 512;
|
private const int PropertyStateBufferMaxCache = 512;
|
||||||
private const int InitialInternCapacity = 32;
|
private const int InitialInternCapacity = 32;
|
||||||
|
|
@ -186,7 +187,7 @@ public static partial class AcBinarySerializer
|
||||||
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
|
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
|
||||||
// Kumulatívan gyűjtjük a benchmark során.
|
// 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);
|
ArrayPool<byte>.Shared.Return(_buffer);
|
||||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||||
|
|
@ -214,6 +215,14 @@ public static partial class AcBinarySerializer
|
||||||
_propertyStateBuffer = null;
|
_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
|
// Clear wrapper tracking - returns IdentityMap arrays to pool
|
||||||
base.Clear();
|
base.Clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ public static partial class AcBinarySerializer
|
||||||
// 1. It's IId (can be deduplicated by Id)
|
// 1. It's IId (can be deduplicated by Id)
|
||||||
// 2. It has complex properties (children could be shared)
|
// 2. It has complex properties (children could be shared)
|
||||||
// 3. It's not a primitive/string (could be referenced multiple times)
|
// 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
|
// Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute
|
||||||
if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))
|
if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using static AyCode.Core.Helpers.JsonUtilities;
|
using static AyCode.Core.Helpers.JsonUtilities;
|
||||||
|
|
||||||
namespace AyCode.Core.Serializers.Binaries;
|
namespace AyCode.Core.Serializers.Binaries;
|
||||||
|
|
@ -8,7 +9,10 @@ public static partial class AcBinarySerializer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// First pass: scans object graph to identify duplicates (strings + objects).
|
/// First pass: scans object graph to identify duplicates (strings + objects).
|
||||||
/// Only traverses reference properties (complex types + strings).
|
/// 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).
|
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context)
|
private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context)
|
||||||
|
|
@ -16,41 +20,43 @@ public static partial class AcBinarySerializer
|
||||||
if (!context.HasCaching)
|
if (!context.HasCaching)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ScanValue(value, type, context, 0);
|
var wrapper = context.GetWrapper(type);
|
||||||
// No AssignCacheIndicesInOrder() needed - CacheIndex assigned inline on 2nd occurrence
|
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)
|
if (value == null || depth > context.MaxDepth)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// String → intern tracking (with length check to match serialize pass)
|
var metadata = wrapper.Metadata;
|
||||||
if (value is string str)
|
|
||||||
{
|
|
||||||
if (context.UseStringInterning && context.IsValidForInterningString(str.Length))
|
|
||||||
{
|
|
||||||
context.ScanInternString(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
// Skip primitives (pre-computed field, no Type.GetTypeCode() call)
|
||||||
}
|
if (metadata.IsPrimitiveType)
|
||||||
|
|
||||||
// Skip primitives
|
|
||||||
if (IsPrimitiveOrStringFast(type))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Collection → iterate elements
|
// Collection → iterate elements using IList fast path (no IEnumerator alloc)
|
||||||
if (value is IEnumerable enumerable)
|
if (metadata.IsCollection)
|
||||||
{
|
{
|
||||||
var elementType = GetCollectionElementType(type) ?? typeof(object);
|
if (metadata.ElementNeedsScan)
|
||||||
if (!IsPrimitiveOrStringFast(elementType) || elementType == typeof(string))
|
|
||||||
{
|
{
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
foreach (var item in enumerable)
|
if (value is IList list)
|
||||||
{
|
{
|
||||||
if (item != null)
|
for (var i = 0; i < list.Count; i++)
|
||||||
ScanValue(item, item.GetType(), context, nextDepth);
|
{
|
||||||
|
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
|
// 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)
|
// 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))
|
if (context.UseTypeReferenceHandling(metadata))
|
||||||
{
|
{
|
||||||
// Direct tracking call - avoid extra indirection through context
|
// Direct tracking call - avoid extra indirection through context
|
||||||
|
|
@ -86,7 +94,7 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isFirst)
|
if (!isFirst)
|
||||||
return; // 2nd occurrence → skip children
|
return; // 2nd occurrence → skip children (symmetric with write pass ObjectRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive scan on reference properties only
|
// Recursive scan on reference properties only
|
||||||
|
|
@ -105,11 +113,32 @@ public static partial class AcBinarySerializer
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Object property: use generic getter
|
// Object property: use generic getter, get wrapper for property type
|
||||||
var propValue = prop.GetValue(value);
|
var propValue = prop.GetValue(value);
|
||||||
if (propValue != null)
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -432,15 +432,18 @@ public static partial class AcBinarySerializer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get wrapper once — used by both WriteArray and WriteObject
|
||||||
|
var wrapper = context.GetWrapper(type);
|
||||||
|
|
||||||
// Handle collections/arrays
|
// Handle collections/arrays
|
||||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||||
{
|
{
|
||||||
WriteArray(enumerable, type, context, depth);
|
WriteArray(enumerable, wrapper, context, depth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle complex objects with single-pass reference tracking
|
// 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>
|
/// <summary>
|
||||||
|
|
@ -789,9 +792,8 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
#region Complex Type Writers
|
#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;
|
var metadata = wrapper.Metadata;
|
||||||
|
|
||||||
// Wire format:
|
// Wire format:
|
||||||
|
|
@ -1269,14 +1271,17 @@ public static partial class AcBinarySerializer
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized array writer with specialized paths for primitive arrays.
|
/// Optimized array writer with specialized paths for primitive arrays.
|
||||||
/// </summary>
|
/// </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);
|
context.WriteByte(BinaryTypeCode.Array);
|
||||||
var nextDepth = depth + 1;
|
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
|
// Optimized path for primitive arrays
|
||||||
var elementType = GetCollectionElementType(type);
|
if (elementType != null && metadata.SourceType.IsArray)
|
||||||
if (elementType != null && type.IsArray)
|
|
||||||
{
|
{
|
||||||
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AyCode.Core.Helpers;
|
using AyCode.Core.Helpers;
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -123,6 +124,37 @@ public abstract class TypeMetadataBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool NeedsReferenceTracking { get; protected set; }
|
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
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -158,6 +190,20 @@ public abstract class TypeMetadataBase
|
||||||
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
|
WritableProperties = allReadable.Where(p => p.CanWrite).ToArray();
|
||||||
|
|
||||||
IsComplexType = IsComplexType2(type);
|
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!
|
// Cache IId info at construction time - no runtime reflection needed later!
|
||||||
var idInfo = GetIdInfo(type);
|
var idInfo = GetIdInfo(type);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue