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)
|
||||
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)...");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue