diff --git a/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs b/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs
index 356f8fb..62317d5 100644
--- a/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs
+++ b/AyCode.Core.Tests/Serialization/AcBinarySerializerDiagnosticTests.cs
@@ -230,10 +230,7 @@ public class AcBinarySerializerDiagnosticTests
///
/// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
- /// This test verifies what happens when:
- /// 1. Metadata header registers ALL properties including StockTakingItems
- /// 2. Body SKIPS StockTakingItems because it's null
- /// 3. Deserializer reads the body and must correctly map indices
+ /// This test verifies that property-index-based serialization correctly handles null properties.
///
[TestMethod]
public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
@@ -262,20 +259,13 @@ public class AcBinarySerializerDiagnosticTests
var marker = binary[pos++];
Console.WriteLine($"Marker: 0x{marker:X2}");
- // Read property count from metadata header
- if ((marker & 0x10) != 0) // HasMetadata flag
+ // Skip any header data (strings interning, etc.)
+ // New format uses PropertyIndex directly - no metadata header with property names
+
+ // Find Object marker (0x19)
+ while (pos < binary.Length && binary[pos] != 0x19)
{
- var propCount = binary[pos++];
- Console.WriteLine($"\n=== METADATA HEADER ===");
- Console.WriteLine($"Property count in header: {propCount}");
-
- for (int i = 0; i < propCount; i++)
- {
- var strLen = binary[pos++];
- var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
- pos += strLen;
- Console.WriteLine($" Header property [{i}]: '{propName}'");
- }
+ pos++;
}
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
@@ -284,6 +274,7 @@ public class AcBinarySerializerDiagnosticTests
var bodyStart = pos;
var objectMarker = binary[pos++];
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)");
+ Assert.AreEqual(0x19, objectMarker, "Object marker should be 0x19");
// Read ref ID (if reference handling is enabled)
// VarInt: if top bit is set, continue reading
@@ -306,11 +297,11 @@ public class AcBinarySerializerDiagnosticTests
var bodyPropCount = binary[pos++];
Console.WriteLine($"Property count in body: {bodyPropCount}");
- Console.WriteLine($"\n=== BODY PROPERTIES ===");
+ Console.WriteLine($"\n=== BODY PROPERTIES (PropertyIndex format) ===");
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
{
- var propIndex = binary[pos++];
- Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}");
+ var propIndex = binary[pos++]; // This is now PropertyIndex (alphabetical order)
+ Console.WriteLine($" Body property [{i}]: PropertyIndex={propIndex}");
// Skip the value (simplified - just log)
var valueType = binary[pos];
@@ -342,16 +333,6 @@ public class AcBinarySerializerDiagnosticTests
}
}
- // Find where 0xD6 (Creator = 6) appears in the body
- Console.WriteLine($"\n=== 0xD6 OCCURRENCES ===");
- for (int i = bodyStart; i < binary.Length; i++)
- {
- if (binary[i] == 0xD6)
- {
- Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}");
- }
- }
-
// Deserialize and verify
var result = binary.BinaryTo();
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
index a2b30fd..92550e2 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializeTypeMetadata.cs
@@ -2,51 +2,44 @@ using System.Collections;
using System.Collections.Frozen;
using System.Reflection;
using System.Runtime.CompilerServices;
-using System.Text;
-using static AyCode.Core.Helpers.JsonUtilities;
+using AyCode.Core.Serializers;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinaryDeserializer
{
- internal sealed class BinaryDeserializeTypeMetadata : TypeMetadataBase
+ internal sealed class BinaryDeserializeTypeMetadata : BinaryTypeMetadataBase
{
private readonly FrozenDictionary _properties;
+ ///
+ /// Properties array ordered alphabetically by name for index-based lookup.
+ /// This matches the serializer's ordering, enabling O(1) array access.
+ ///
public BinaryPropertySetterInfo[] PropertiesArray { get; }
-
- ///
- /// Whether this type implements IId interface.
- ///
- public bool IsIId { get; }
-
- ///
- /// Compiled getter for the Id property (if IsIId is true).
- ///
- public Func? IdGetter { get; }
public BinaryDeserializeTypeMetadata(Type type) : base(type)
{
- PropertiesArray = GetSerializableProperties(type, requiresRead: true, requiresWrite: true)
- .Where(static p => p.GetMethod is { IsPublic: true } && p.SetMethod is { IsPublic: true })
- .Select(static p => new BinaryPropertySetterInfo(p, p.DeclaringType!))
- .ToArray();
+ var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: true);
+
+ PropertiesArray = new BinaryPropertySetterInfo[orderedProperties.Length];
+ for (var i = 0; i < orderedProperties.Length; i++)
+ {
+ PropertiesArray[i] = new BinaryPropertySetterInfo(orderedProperties[i], type);
+ }
_properties = PropertiesArray.Length == 0
? FrozenDictionary.Empty
: PropertiesArray.ToFrozenDictionary(static p => p.Name, static p => p, StringComparer.Ordinal);
-
- // Check if type implements IId
- var (isIId, _) = GetIdInfo(type);
- IsIId = isIId;
- if (isIId)
- {
- var idProp = type.GetProperty("Id");
- if (idProp != null)
- IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp);
- }
}
+ ///
+ /// Gets property by index (O(1) array access). Used for index-based deserialization.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public BinaryPropertySetterInfo? GetPropertyByIndex(int index)
+ => (uint)index < (uint)PropertiesArray.Length ? PropertiesArray[index] : null;
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetProperty(string name, out BinaryPropertySetterInfo? propertyInfo)
=> _properties.TryGetValue(name, out propertyInfo);
@@ -87,7 +80,7 @@ public static partial class AcBinaryDeserializer
Type? elementType,
Type? elementIdType,
Func? elementIdGetter)
- : base(CreateDummyProperty(), typeof(object))
+ : base(CreateDummyProperty(), typeof(DummyClass))
{
_isManualConstruction = true;
_manualName = name;
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
new file mode 100644
index 0000000..dabe3df
--- /dev/null
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
@@ -0,0 +1,497 @@
+using System.Collections;
+using System.Runtime.CompilerServices;
+using AyCode.Core.Helpers;
+using static AyCode.Core.Helpers.JsonUtilities;
+
+namespace AyCode.Core.Serializers.Binaries;
+
+public static partial class AcBinaryDeserializer
+{
+ #region Populate Object Methods
+
+ private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
+ {
+ var metadata = GetTypeMetadata(targetType);
+
+ // Skip ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ {
+ context.RegisterObject(refId, target);
+ }
+ }
+
+ PopulateObject(ref context, target, metadata, depth);
+ }
+
+ private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth)
+ {
+ var propertyCount = (int)context.ReadVarUInt();
+ var nextDepth = depth + 1;
+ var targetType = target.GetType();
+ var targetTypeName = targetType.Name;
+
+ for (int i = 0; i < propertyCount; i++)
+ {
+ var propertyIndexStartPosition = context.Position;
+
+ // Read property index directly - no string lookup needed!
+ // PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
+ var propIndex = (int)context.ReadVarUInt();
+
+ // O(1) array lookup instead of dictionary lookup
+ var propInfo = metadata.GetPropertyByIndex(propIndex);
+
+ if (propInfo == null)
+ {
+ // Skip unknown property (schema evolution - new property on sender side)
+ SkipValue(ref context);
+ continue;
+ }
+
+ // OPTIMIZATION: Reuse existing nested objects instead of creating new ones
+ var peekCode = context.PeekByte();
+
+ // Handle nested complex objects - reuse existing if available
+ if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
+ {
+ var existingObj = propInfo.GetValue(target);
+ if (existingObj != null)
+ {
+ context.ReadByte(); // consume Object marker
+ PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
+ continue;
+ }
+ }
+
+ // Handle collections - reuse existing collection and populate items
+ if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
+ {
+ var existingCollection = propInfo.GetValue(target);
+ if (existingCollection is IList existingList)
+ {
+ context.ReadByte(); // consume Array marker
+ PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
+ continue;
+ }
+ }
+
+ // Default: read value and set (for primitives, strings, null cases)
+ var positionBeforeRead = context.Position;
+ try
+ {
+ // OPTIMIZATION: Use typed setters for primitives to avoid boxing
+ if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
+ continue;
+
+ var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
+ propInfo.SetValue(target, value);
+ }
+ catch (InvalidCastException ex)
+ {
+ // Add context about which property and what byte code was at the read position
+ throw new AcBinaryDeserializationException(
+ $"Type mismatch for property '{propInfo.Name}' (index {i}/{propertyCount}, propIndex={propIndex}) on '{targetTypeName}'. " +
+ $"Expected type: '{propInfo.PropertyType.FullName}'. " +
+ $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
+ $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
+ $"Depth: {depth}. " +
+ $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
+ $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
+ $"Error: {ex.Message}",
+ positionBeforeRead,
+ propInfo.PropertyType,
+ ex);
+ }
+ }
+ }
+
+ ///
+ /// Populate nested object, reusing existing object and recursively updating properties.
+ ///
+ private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
+ {
+ var metadata = GetTypeMetadata(targetType);
+
+ // Handle ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ {
+ context.RegisterObject(refId, target);
+ }
+ }
+
+ PopulateObject(ref context, target, metadata, depth);
+ }
+
+ private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
+ {
+ var metadata = GetTypeMetadata(targetType);
+
+ // Skip ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ {
+ context.RegisterObject(refId, target);
+ }
+ }
+
+ var propertyCount = (int)context.ReadVarUInt();
+ var nextDepth = depth + 1;
+
+ for (int i = 0; i < propertyCount; i++)
+ {
+ // Read property index directly - O(1) array lookup
+ var propIndex = (int)context.ReadVarUInt();
+ var propInfo = metadata.GetPropertyByIndex(propIndex);
+
+ if (propInfo == null)
+ {
+ SkipValue(ref context);
+ continue;
+ }
+
+ var peekCode = context.PeekByte();
+
+ // Handle IId collection merge
+ if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
+ {
+ var existingCollection = propInfo.GetValue(target);
+ if (existingCollection is IList existingList)
+ {
+ context.ReadByte(); // consume Array marker
+ MergeIIdCollection(ref context, existingList, propInfo, depth);
+ continue;
+ }
+ }
+
+ // Handle nested object merge
+ if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
+ {
+ var existingObj = propInfo.GetValue(target);
+ if (existingObj != null)
+ {
+ context.ReadByte(); // consume Object marker
+ PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth);
+ continue;
+ }
+ }
+
+ var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
+ propInfo.SetValue(target, value);
+ }
+ }
+
+ #endregion
+
+ #region Populate List Methods
+
+ private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
+ {
+ var elementType = GetCollectionElementType(listType) ?? typeof(object);
+
+ var acObservable = targetList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ targetList.Clear();
+
+ var count = (int)context.ReadVarUInt();
+ var nextDepth = depth + 1;
+
+ // ChainMode: Get IId info for element type
+ var isIId = false;
+ Type? idType = null;
+ Func? idGetter = null;
+
+ if (context.IsChainMode)
+ {
+ var idInfo = GetIdInfo(elementType);
+ isIId = idInfo.IsId;
+ idType = idInfo.IdType;
+ if (isIId && idType != null)
+ {
+ var idProp = elementType.GetProperty("Id");
+ if (idProp != null)
+ idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp);
+ }
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ var value = ReadValue(ref context, elementType, nextDepth);
+
+ // ChainMode: Check if we already have this IId object
+ if (context.IsChainMode && value != null && idGetter != null && idType != null)
+ {
+ var id = idGetter(value);
+ if (id != null && !IsDefaultValue(id, idType))
+ {
+ if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
+ {
+ // Use existing object instead of new one
+ targetList.Add(existingObj);
+ continue;
+ }
+ }
+ }
+
+ targetList.Add(value);
+ }
+ }
+ finally
+ {
+ acObservable?.EndUpdate();
+ }
+ }
+
+ private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
+ {
+ var elementType = GetCollectionElementType(listType) ?? typeof(object);
+ var (isId, idType) = GetIdInfo(elementType);
+
+ if (!isId || idType == null)
+ {
+ // No IId, just replace
+ PopulateList(ref context, targetList, listType, depth);
+ return;
+ }
+
+ // IId merge logic
+ var idProp = elementType.GetProperty("Id");
+ if (idProp == null)
+ {
+ PopulateList(ref context, targetList, listType, depth);
+ return;
+ }
+
+ var idGetter = CreateCompiledGetter(elementType, idProp);
+ var propInfo = new BinaryPropertySetterInfo(
+ "Items", elementType, true, elementType, idType, idGetter);
+
+ MergeIIdCollection(ref context, targetList, propInfo, depth);
+ }
+
+ ///
+ /// Optimized list populate that reuses existing items when possible.
+ ///
+ private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
+ {
+ var elementType = propInfo.ElementType ?? typeof(object);
+ var count = (int)context.ReadVarUInt();
+ var nextDepth = depth + 1;
+
+ var acObservable = existingList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ var existingCount = existingList.Count;
+ var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null;
+
+ for (int i = 0; i < count; i++)
+ {
+ var peekCode = context.PeekByte();
+
+ // If we have an existing item at this index and the incoming is an object, reuse it
+ if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
+ {
+ var existingItem = existingList[i];
+ if (existingItem != null)
+ {
+ context.ReadByte(); // consume Object marker
+
+ // Handle ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ {
+ context.RegisterObject(refId, existingItem);
+ }
+ }
+
+ PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
+ continue;
+ }
+ }
+
+ // Read new value
+ var value = ReadValue(ref context, elementType, nextDepth);
+
+ if (i < existingCount)
+ {
+ // Replace existing item
+ existingList[i] = value;
+ }
+ else
+ {
+ // Add new item
+ existingList.Add(value);
+ }
+ }
+
+ // Remove extra items if new list is shorter
+ while (existingList.Count > count)
+ {
+ existingList.RemoveAt(existingList.Count - 1);
+ }
+ }
+ finally
+ {
+ acObservable?.EndUpdate();
+ }
+ }
+
+ #endregion
+
+ #region Merge Methods
+
+ ///
+ /// Optimized IId collection merge with capacity hints and reduced boxing.
+ ///
+ private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
+ {
+ var elementType = propInfo.ElementType!;
+ var idGetter = propInfo.ElementIdGetter!;
+ var idType = propInfo.ElementIdType!;
+
+ var count = existingList.Count;
+ var acObservable = existingList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ // Build lookup dictionary with capacity hint
+ Dictionary? existingById = null;
+ if (count > 0)
+ {
+ existingById = new Dictionary(count);
+ for (var idx = 0; idx < count; idx++)
+ {
+ var item = existingList[idx];
+ if (item != null)
+ {
+ var id = idGetter(item);
+ if (id != null && !IsDefaultValue(id, idType))
+ existingById[id] = item;
+ }
+ }
+ }
+
+ var arrayCount = (int)context.ReadVarUInt();
+ var nextDepth = depth + 1;
+ var elementMetadata = GetTypeMetadata(elementType);
+
+ // Track which IDs we see in source (for orphan removal)
+ HashSet? sourceIds = context.RemoveOrphanedItems && existingById != null
+ ? new HashSet(arrayCount)
+ : null;
+
+ for (int i = 0; i < arrayCount; i++)
+ {
+ var itemCode = context.PeekByte();
+ if (itemCode != BinaryTypeCode.Object)
+ {
+ var value = ReadValue(ref context, elementType, nextDepth);
+ if (value != null)
+ existingList.Add(value);
+ continue;
+ }
+
+ context.ReadByte(); // consume Object marker
+ var newItem = CreateInstance(elementType, elementMetadata);
+ if (newItem == null) continue;
+
+ // Read ref ID if present
+ if (context.HasReferenceHandling)
+ {
+ var refId = context.ReadVarInt();
+ if (refId > 0)
+ context.RegisterObject(refId, newItem);
+ }
+
+ PopulateObject(ref context, newItem, elementMetadata, nextDepth);
+
+ var itemId = idGetter(newItem);
+ if (itemId != null && !IsDefaultValue(itemId, idType))
+ {
+ // Track this ID as seen in source
+ sourceIds?.Add(itemId);
+
+ if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
+ {
+ // Copy properties to existing item
+ CopyProperties(newItem, existingItem, elementMetadata);
+ continue;
+ }
+ }
+
+ existingList.Add(newItem);
+ }
+
+ // Remove orphaned items (items in destination but not in source)
+ if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
+ {
+ // Find items to remove (those not in sourceIds)
+ var itemsToRemove = new List();
+ foreach (var kvp in existingById)
+ {
+ if (!sourceIds.Contains(kvp.Key))
+ {
+ itemsToRemove.Add(kvp.Value);
+ }
+ }
+
+ // Remove orphaned items
+ foreach (var item in itemsToRemove)
+ {
+ existingList.Remove(item);
+ }
+ }
+ }
+ finally
+ {
+ acObservable?.EndUpdate();
+ }
+ }
+
+ private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
+ {
+ var props = metadata.PropertiesArray;
+ for (var i = 0; i < props.Length; i++)
+ {
+ var prop = props[i];
+ var value = prop.GetValue(source);
+ if (value != null)
+ prop.SetValue(target, value);
+ }
+ }
+
+ ///
+ /// Determines if a type is a complex type (not primitive, string, or simple value type).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsComplexType(Type type)
+ {
+ if (type.IsPrimitive) return false;
+ if (ReferenceEquals(type, StringType)) return false;
+ if (type.IsEnum) return false;
+ if (ReferenceEquals(type, GuidType)) return false;
+ if (ReferenceEquals(type, DateTimeType)) return false;
+ if (ReferenceEquals(type, DecimalType)) return false;
+ if (ReferenceEquals(type, TimeSpanType)) return false;
+ if (ReferenceEquals(type, DateTimeOffsetType)) return false;
+ if (Nullable.GetUnderlyingType(type) != null) return false;
+ return true;
+ }
+
+ #endregion
+}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index 80db678..f23c933 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -43,6 +43,28 @@ public static partial class AcBinaryDeserializer
{
private static readonly ConcurrentDictionary TypeMetadataCache = new();
private static readonly ConcurrentDictionary TypeConversionCache = new();
+
+ ///
+ /// ThreadLocal cache for hot path type metadata lookup.
+ /// Avoids ConcurrentDictionary overhead for frequently accessed types.
+ ///
+ [ThreadStatic]
+ private static Dictionary? t_deserializeTypeMetadataLocalCache;
+
+ ///
+ /// ThreadLocal cache for type conversion info.
+ ///
+ [ThreadStatic]
+ private static Dictionary? t_typeConversionLocalCache;
+
+ ///
+ /// Maximum local cache size before clearing.
+ /// Clear() is preferred over new Dictionary() because:
+ /// 1. Reuses already allocated internal array (no new allocation)
+ /// 2. Reduces GC pressure
+ /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
+ ///
+ private const int MaxLocalCacheSize = 64;
// Type dispatch table for fast ReadValue
private delegate object? TypeReader(ref BinaryDeserializationContext context, Type targetType, int depth);
@@ -819,13 +841,44 @@ public static partial class AcBinaryDeserializer
};
}
+ ///
+ /// Gets type conversion info with ThreadLocal caching.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeConversionInfo GetConversionInfo(Type targetType)
- => TypeConversionCache.GetOrAdd(targetType, static type =>
+ {
+ // Fast path: check ThreadLocal cache first
+ var localCache = t_typeConversionLocalCache;
+ if (localCache != null && localCache.TryGetValue(targetType, out var cached))
+ {
+ return cached;
+ }
+
+ // Slow path
+ return GetConversionInfoSlow(targetType);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static TypeConversionInfo GetConversionInfoSlow(Type targetType)
+ {
+ var info = TypeConversionCache.GetOrAdd(targetType, static type =>
{
var underlying = Nullable.GetUnderlyingType(type) ?? type;
return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
});
+
+ // Get or create local cache
+ var localCache = t_typeConversionLocalCache ??= new Dictionary();
+
+ // Clear when full - reuses internal array, better than new Dictionary()
+ if (localCache.Count >= MaxLocalCacheSize)
+ {
+ localCache.Clear();
+ }
+
+ localCache[targetType] = info;
+ return info;
+ }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadEnumValue(ref BinaryDeserializationContext context, Type targetType)
@@ -919,463 +972,6 @@ public static partial class AcBinaryDeserializer
return instance;
}
- private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
- {
- var metadata = GetTypeMetadata(targetType);
-
- // Skip ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(refId, target);
- }
- }
-
- PopulateObject(ref context, target, metadata, depth);
- }
-
- private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth)
- {
- var propertyCount = (int)context.ReadVarUInt();
- var nextDepth = depth + 1;
- var targetType = target.GetType();
- var targetTypeName = targetType.Name;
-
- for (int i = 0; i < propertyCount; i++)
- {
- var propertyNameStartPosition = context.Position;
- string propertyName;
- int propIndex = -1;
- if (context.HasMetadata)
- {
- propIndex = (int)context.ReadVarUInt();
- propertyName = context.GetPropertyName(propIndex);
- }
- else
- {
- var typeCode = context.ReadByte();
- if (typeCode == BinaryTypeCode.String)
- {
- propertyName = ReadAndInternString(ref context);
- }
- else if (typeCode == BinaryTypeCode.StringInterned)
- {
- propertyName = context.GetInternedString((int)context.ReadVarUInt());
- }
- else if (typeCode == BinaryTypeCode.StringEmpty)
- {
- propertyName = string.Empty;
- }
- else if (typeCode == BinaryTypeCode.StringInternNew)
- {
- propertyName = ReadAndRegisterInternedString(ref context);
- }
- else if (BinaryTypeCode.IsFixStr(typeCode))
- {
- // FixStr: short string with length encoded in type code
- var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
- propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length);
- }
- else
- {
- throw new AcBinaryDeserializationException(
- $"Expected string for property name, got: {typeCode} (0x{typeCode:X2}) at position {propertyNameStartPosition}. " +
- $"Target: {targetTypeName}, PropertyIndex: {i}/{propertyCount}, Depth: {depth}",
- context.Position, targetType);
- }
- }
-
- if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
- {
- // Skip unknown property
- SkipValue(ref context);
- continue;
- }
-
- // OPTIMIZATION: Reuse existing nested objects instead of creating new ones
- var peekCode = context.PeekByte();
-
- // Handle nested complex objects - reuse existing if available
- if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType)
- {
- var existingObj = propInfo.GetValue(target);
- if (existingObj != null)
- {
- context.ReadByte(); // consume Object marker
- PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth);
- continue;
- }
- }
-
- // Handle collections - reuse existing collection and populate items
- if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
- {
- var existingCollection = propInfo.GetValue(target);
- if (existingCollection is IList existingList)
- {
- context.ReadByte(); // consume Array marker
- PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
- continue;
- }
- }
-
- // Default: read value and set (for primitives, strings, null cases)
- var positionBeforeRead = context.Position;
- try
- {
- // OPTIMIZATION: Use typed setters for primitives to avoid boxing
- if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
- continue;
-
- var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
- propInfo.SetValue(target, value);
- }
- catch (InvalidCastException ex)
- {
- // Add context about which property and what byte code was at the read position
- throw new AcBinaryDeserializationException(
- $"Type mismatch for property '{propertyName}' (index {i}/{propertyCount}, headerIndex={propIndex}) on '{targetTypeName}'. " +
- $"Expected type: '{propInfo.PropertyType.FullName}'. " +
- $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
- $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
- $"Depth: {depth}. " +
- $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
- $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
- $"Error: {ex.Message}",
- positionBeforeRead,
- propInfo.PropertyType,
- ex);
- }
- }
- }
-
- ///
- /// Populate nested object, reusing existing object and recursively updating properties.
- ///
- private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
- {
- var metadata = GetTypeMetadata(targetType);
-
- // Handle ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(refId, target);
- }
- }
-
- PopulateObject(ref context, target, metadata, depth);
- }
-
- private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
- {
- var metadata = GetTypeMetadata(targetType);
-
- // Skip ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(refId, target);
- }
- }
-
- var propertyCount = (int)context.ReadVarUInt();
- var nextDepth = depth + 1;
-
- for (int i = 0; i < propertyCount; i++)
- {
- string propertyName;
- if (context.HasMetadata)
- {
- var propIndex = (int)context.ReadVarUInt();
- propertyName = context.GetPropertyName(propIndex);
- }
- else
- {
- var typeCode = context.ReadByte();
- if (typeCode == BinaryTypeCode.String)
- {
- propertyName = ReadAndInternString(ref context);
- }
- else if (typeCode == BinaryTypeCode.StringInterned)
- {
- propertyName = context.GetInternedString((int)context.ReadVarUInt());
- }
- else if (typeCode == BinaryTypeCode.StringEmpty)
- {
- propertyName = string.Empty;
- }
- else if (typeCode == BinaryTypeCode.StringInternNew)
- {
- propertyName = ReadAndRegisterInternedString(ref context);
- }
- else if (BinaryTypeCode.IsFixStr(typeCode))
- {
- // FixStr: short string with length encoded in type code
- var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
- propertyName = length == 0 ? string.Empty : context.ReadStringUtf8(length);
- }
- else
- {
- throw new AcBinaryDeserializationException(
- $"Expected string for property name, got: {typeCode} (0x{typeCode:X2})",
- context.Position, targetType);
- }
- }
-
- if (!metadata.TryGetProperty(propertyName, out var propInfo) || propInfo == null)
- {
- SkipValue(ref context);
- continue;
- }
-
- var peekCode = context.PeekByte();
-
- // Handle IId collection merge
- if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
- {
- var existingCollection = propInfo.GetValue(target);
- if (existingCollection is IList existingList)
- {
- context.ReadByte(); // consume Array marker
- MergeIIdCollection(ref context, existingList, propInfo, depth);
- continue;
- }
- }
-
- // Handle nested object merge
- if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
- {
- var existingObj = propInfo.GetValue(target);
- if (existingObj != null)
- {
- context.ReadByte(); // consume Object marker
- PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth);
- continue;
- }
- }
-
- var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
- propInfo.SetValue(target, value);
- }
- }
-
- ///
- /// Optimized IId collection merge with capacity hints and reduced boxing.
- ///
- private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
- {
- var elementType = propInfo.ElementType!;
- var idGetter = propInfo.ElementIdGetter!;
- var idType = propInfo.ElementIdType!;
-
- var count = existingList.Count;
- var acObservable = existingList as IAcObservableCollection;
- acObservable?.BeginUpdate();
-
- try
- {
- // Build lookup dictionary with capacity hint
- Dictionary? existingById = null;
- if (count > 0)
- {
- existingById = new Dictionary(count);
- for (var idx = 0; idx < count; idx++)
- {
- var item = existingList[idx];
- if (item != null)
- {
- var id = idGetter(item);
- if (id != null && !IsDefaultValue(id, idType))
- existingById[id] = item;
- }
- }
- }
-
- var arrayCount = (int)context.ReadVarUInt();
- var nextDepth = depth + 1;
- var elementMetadata = GetTypeMetadata(elementType);
-
- // Track which IDs we see in source (for orphan removal)
- HashSet? sourceIds = context.RemoveOrphanedItems && existingById != null
- ? new HashSet(arrayCount)
- : null;
-
- for (int i = 0; i < arrayCount; i++)
- {
- var itemCode = context.PeekByte();
- if (itemCode != BinaryTypeCode.Object)
- {
- var value = ReadValue(ref context, elementType, nextDepth);
- if (value != null)
- existingList.Add(value);
- continue;
- }
-
- context.ReadByte(); // consume Object marker
- var newItem = CreateInstance(elementType, elementMetadata);
- if (newItem == null) continue;
-
- // Read ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- context.RegisterObject(refId, newItem);
- }
-
- PopulateObject(ref context, newItem, elementMetadata, nextDepth);
-
- var itemId = idGetter(newItem);
- if (itemId != null && !IsDefaultValue(itemId, idType))
- {
- // Track this ID as seen in source
- sourceIds?.Add(itemId);
-
- if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
- {
- // Copy properties to existing item
- CopyProperties(newItem, existingItem, elementMetadata);
- continue;
- }
- }
-
- existingList.Add(newItem);
- }
-
- // Remove orphaned items (items in destination but not in source)
- if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
- {
- // Find items to remove (those not in sourceIds)
- var itemsToRemove = new List();
- foreach (var kvp in existingById)
- {
- if (!sourceIds.Contains(kvp.Key))
- {
- itemsToRemove.Add(kvp.Value);
- }
- }
-
- // Remove orphaned items
- foreach (var item in itemsToRemove)
- {
- existingList.Remove(item);
- }
- }
- }
- finally
- {
- acObservable?.EndUpdate();
- }
- }
-
- private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
- {
- var props = metadata.PropertiesArray;
- for (var i = 0; i < props.Length; i++)
- {
- var prop = props[i];
- var value = prop.GetValue(source);
- if (value != null)
- prop.SetValue(target, value);
- }
- }
-
- ///
- /// Determines if a type is a complex type (not primitive, string, or simple value type).
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsComplexType(Type type)
- {
- if (type.IsPrimitive) return false;
- if (ReferenceEquals(type, StringType)) return false;
- if (type.IsEnum) return false;
- if (ReferenceEquals(type, GuidType)) return false;
- if (ReferenceEquals(type, DateTimeType)) return false;
- if (ReferenceEquals(type, DecimalType)) return false;
- if (ReferenceEquals(type, TimeSpanType)) return false;
- if (ReferenceEquals(type, DateTimeOffsetType)) return false;
- if (Nullable.GetUnderlyingType(type) != null) return false;
- return true;
- }
-
- ///
- /// Optimized list populate that reuses existing items when possible.
- ///
- private static void PopulateListOptimized(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth)
- {
- var elementType = propInfo.ElementType ?? typeof(object);
- var count = (int)context.ReadVarUInt();
- var nextDepth = depth + 1;
-
- var acObservable = existingList as IAcObservableCollection;
- acObservable?.BeginUpdate();
-
- try
- {
- var existingCount = existingList.Count;
- var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null;
-
- for (int i = 0; i < count; i++)
- {
- var peekCode = context.PeekByte();
-
- // If we have an existing item at this index and the incoming is an object, reuse it
- if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
- {
- var existingItem = existingList[i];
- if (existingItem != null)
- {
- context.ReadByte(); // consume Object marker
-
- // Handle ref ID if present
- if (context.HasReferenceHandling)
- {
- var refId = context.ReadVarInt();
- if (refId > 0)
- {
- context.RegisterObject(refId, existingItem);
- }
- }
-
- PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
- continue;
- }
- }
-
- // Read new value
- var value = ReadValue(ref context, elementType, nextDepth);
-
- if (i < existingCount)
- {
- // Replace existing item
- existingList[i] = value;
- }
- else
- {
- // Add new item
- existingList.Add(value);
- }
- }
-
- // Remove extra items if new list is shorter
- while (existingList.Count > count)
- {
- existingList.RemoveAt(existingList.Count - 1);
- }
- }
- finally
- {
- acObservable?.EndUpdate();
- }
- }
-
#endregion
#region Array Reading
@@ -1557,93 +1153,6 @@ public static partial class AcBinaryDeserializer
return null;
}
- private static void PopulateList(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
- {
- var elementType = GetCollectionElementType(listType) ?? typeof(object);
-
- var acObservable = targetList as IAcObservableCollection;
- acObservable?.BeginUpdate();
-
- try
- {
- targetList.Clear();
-
- var count = (int)context.ReadVarUInt();
- var nextDepth = depth + 1;
-
- // ChainMode: Get IId info for element type
- var isIId = false;
- Type? idType = null;
- Func? idGetter = null;
-
- if (context.IsChainMode)
- {
- var idInfo = GetIdInfo(elementType);
- isIId = idInfo.IsId;
- idType = idInfo.IdType;
- if (isIId && idType != null)
- {
- var idProp = elementType.GetProperty("Id");
- if (idProp != null)
- idGetter = AcSerializerCommon.CreateCompiledGetter(elementType, idProp);
- }
- }
-
- for (int i = 0; i < count; i++)
- {
- var value = ReadValue(ref context, elementType, nextDepth);
-
- // ChainMode: Check if we already have this IId object
- if (context.IsChainMode && value != null && idGetter != null && idType != null)
- {
- var id = idGetter(value);
- if (id != null && !IsDefaultValue(id, idType))
- {
- if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj))
- {
- // Use existing object instead of new one
- targetList.Add(existingObj);
- continue;
- }
- }
- }
-
- targetList.Add(value);
- }
- }
- finally
- {
- acObservable?.EndUpdate();
- }
- }
-
- private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth)
- {
- var elementType = GetCollectionElementType(listType) ?? typeof(object);
- var (isId, idType) = GetIdInfo(elementType);
-
- if (!isId || idType == null)
- {
- // No IId, just replace
- PopulateList(ref context, targetList, listType, depth);
- return;
- }
-
- // IId merge logic
- var idProp = elementType.GetProperty("Id");
- if (idProp == null)
- {
- PopulateList(ref context, targetList, listType, depth);
- return;
- }
-
- var idGetter = CreateCompiledGetter(elementType, idProp);
- var propInfo = new BinaryPropertySetterInfo(
- "Items", elementType, true, elementType, idType, idGetter);
-
- MergeIIdCollection(ref context, targetList, propInfo, depth);
- }
-
#endregion
#region Dictionary Reading
@@ -1830,38 +1339,8 @@ public static partial class AcBinaryDeserializer
var propCount = (int)context.ReadVarUInt();
for (int i = 0; i < propCount; i++)
{
- // Skip property name - but must register in intern table!
- if (context.HasMetadata)
- {
- context.ReadVarUInt();
- }
- else
- {
- var nameCode = context.ReadByte();
- if (nameCode == BinaryTypeCode.String)
- {
- // CRITICAL FIX: Must register property name in intern table even when skipping!
- SkipAndInternString(ref context);
- }
- else if (nameCode == BinaryTypeCode.StringInterned)
- {
- // Just read the index, no registration needed
- context.ReadVarUInt();
- }
- else if (nameCode == BinaryTypeCode.StringInternNew)
- {
- // New interned string - must register even when skipping!
- SkipAndRegisterInternedString(ref context);
- }
- else if (BinaryTypeCode.IsFixStr(nameCode))
- {
- // FixStr: short string, just skip the bytes
- var length = BinaryTypeCode.DecodeFixStrLength(nameCode);
- if (length > 0)
- context.Skip(length);
- }
- // StringEmpty doesn't need any action
- }
+ // Skip property index (VarUInt)
+ context.ReadVarUInt();
// Skip value
SkipValue(ref context);
@@ -1891,9 +1370,40 @@ public static partial class AcBinaryDeserializer
#region Type Metadata
+ ///
+ /// Gets type metadata with ThreadLocal caching for hot path optimization.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
- => TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
+ {
+ // Fast path: check ThreadLocal cache first
+ var localCache = t_deserializeTypeMetadataLocalCache;
+ if (localCache != null && localCache.TryGetValue(type, out var cached))
+ {
+ return cached;
+ }
+
+ // Slow path: get from global cache and populate local cache
+ return GetTypeMetadataSlow(type);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static BinaryDeserializeTypeMetadata GetTypeMetadataSlow(Type type)
+ {
+ var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));
+
+ // Get or create local cache
+ var localCache = t_deserializeTypeMetadataLocalCache ??= new Dictionary();
+
+ // Clear when full - reuses internal array, better than new Dictionary()
+ if (localCache.Count >= MaxLocalCacheSize)
+ {
+ localCache.Clear();
+ }
+
+ localCache[type] = metadata;
+ return metadata;
+ }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
index 64b83f0..4d7223b 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs
@@ -138,6 +138,7 @@ public static partial class AcBinarySerializer
_propertyNameList?.Clear();
_internedStringList?.Clear();
+ _internedStringUtf8?.Clear();
// Reset cached property indices
ResetCachedPropertyIndices();
@@ -186,11 +187,17 @@ public static partial class AcBinarySerializer
#region String Interning
+ ///
+ /// Cached UTF8 bytes for interned strings to avoid re-encoding in FinalizeHeaderSections.
+ ///
+ private List? _internedStringUtf8;
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int RegisterInternedString(string value)
{
_internedStrings ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal);
_internedStringList ??= new List(InitialInternCapacity);
+ _internedStringUtf8 ??= new List(InitialInternCapacity);
// Fast path: check bloom filter first
var hash = GetStringHash(value);
@@ -200,6 +207,8 @@ public static partial class AcBinarySerializer
var newIndex = _internedStringList.Count;
_internedStrings[value] = newIndex;
_internedStringList.Add(value);
+ // Cache UTF8 bytes immediately
+ _internedStringUtf8.Add(GetUtf8BytesCached(value));
BloomFilterAdd(hash);
return newIndex;
}
@@ -213,10 +222,29 @@ public static partial class AcBinarySerializer
index = _internedStringList.Count;
_internedStringList.Add(value);
+ // Cache UTF8 bytes immediately
+ _internedStringUtf8.Add(GetUtf8BytesCached(value));
BloomFilterAdd(hash);
return index;
}
+ ///
+ /// Get UTF8 bytes for a string, optimized for ASCII strings.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static byte[] GetUtf8BytesCached(string value)
+ {
+ // Fast path for ASCII strings - direct char to byte conversion
+ if (System.Text.Ascii.IsValid(value))
+ {
+ var bytes = new byte[value.Length];
+ System.Text.Ascii.FromUtf16(value.AsSpan(), bytes, out _);
+ return bytes;
+ }
+ // Standard path for multi-byte UTF8
+ return Utf8NoBom.GetBytes(value);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetStringHash(string value)
{
@@ -989,14 +1017,27 @@ public static partial class AcBinarySerializer
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 };
var hasInternTable = UseStringInterning && _internedStringList is { Count: > 0 };
- // Calculate actual header size first
+ // Fast path: no header payload needed
+ if (!hasPropertyNames && !hasInternTable)
+ {
+ // Write header flags only
+ byte flags = BinaryTypeCode.HeaderFlagsBase;
+ if (UseReferenceHandling)
+ flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
+
+ _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
+ _buffer[_headerPosition + 1] = flags;
+ return;
+ }
+
+ // Calculate actual header size using cached UTF8 bytes
var actualSize = 0;
if (hasPropertyNames)
{
actualSize += GetVarUIntSize((uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
{
- var byteCount = Utf8NoBom.GetByteCount(name);
+ var byteCount = System.Text.Ascii.IsValid(name) ? name.Length : Utf8NoBom.GetByteCount(name);
actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
}
}
@@ -1004,10 +1045,11 @@ public static partial class AcBinarySerializer
if (hasInternTable)
{
actualSize += GetVarUIntSize((uint)_internedStringList!.Count);
- foreach (var value in _internedStringList)
+ // Use cached UTF8 byte lengths
+ for (var i = 0; i < _internedStringUtf8!.Count; i++)
{
- var byteCount = Utf8NoBom.GetByteCount(value);
- actualSize += GetVarUIntSize((uint)byteCount) + byteCount;
+ var utf8Bytes = _internedStringUtf8[i];
+ actualSize += GetVarUIntSize((uint)utf8Bytes.Length) + utf8Bytes.Length;
}
}
@@ -1031,7 +1073,7 @@ public static partial class AcBinarySerializer
}
}
- // Write header payload directly to buffer (no ArrayBufferWriter allocation)
+ // Write header payload directly to buffer using cached UTF8 bytes
var headerPos = _headerPosition + 2;
if (hasPropertyNames)
@@ -1039,30 +1081,54 @@ public static partial class AcBinarySerializer
headerPos = WriteVarUIntAt(headerPos, (uint)_propertyNameList!.Count);
foreach (var name in _propertyNameList)
{
- headerPos = WriteStringAt(headerPos, name);
+ headerPos = WriteStringAtOptimized(headerPos, name);
}
}
if (hasInternTable)
{
headerPos = WriteVarUIntAt(headerPos, (uint)_internedStringList!.Count);
- foreach (var value in _internedStringList)
+ // Use cached UTF8 bytes - no re-encoding needed!
+ for (var i = 0; i < _internedStringUtf8!.Count; i++)
{
- headerPos = WriteStringAt(headerPos, value);
+ var utf8Bytes = _internedStringUtf8[i];
+ headerPos = WriteVarUIntAt(headerPos, (uint)utf8Bytes.Length);
+ utf8Bytes.CopyTo(_buffer.AsSpan(headerPos));
+ headerPos += utf8Bytes.Length;
}
}
// Write header flags
- byte flags = BinaryTypeCode.HeaderFlagsBase;
+ byte flags2 = BinaryTypeCode.HeaderFlagsBase;
if (hasPropertyNames)
- flags |= BinaryTypeCode.HeaderFlag_Metadata;
+ flags2 |= BinaryTypeCode.HeaderFlag_Metadata;
if (UseReferenceHandling)
- flags |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
+ flags2 |= BinaryTypeCode.HeaderFlag_ReferenceHandling;
if (hasInternTable)
- flags |= BinaryTypeCode.HeaderFlag_StringInternTable;
+ flags2 |= BinaryTypeCode.HeaderFlag_StringInternTable;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
- _buffer[_headerPosition + 1] = flags;
+ _buffer[_headerPosition + 1] = flags2;
+ }
+
+ ///
+ /// Writes UTF8 string at specific position, optimized for ASCII strings.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int WriteStringAtOptimized(int pos, string value)
+ {
+ // Fast path for ASCII strings
+ if (System.Text.Ascii.IsValid(value))
+ {
+ pos = WriteVarUIntAt(pos, (uint)value.Length);
+ System.Text.Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(pos, value.Length), out _);
+ return pos + value.Length;
+ }
+ // Standard path for multi-byte UTF8
+ var byteCount = Utf8NoBom.GetByteCount(value);
+ pos = WriteVarUIntAt(pos, (uint)byteCount);
+ Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
+ return pos + byteCount;
}
///
@@ -1080,20 +1146,6 @@ public static partial class AcBinarySerializer
return pos;
}
- ///
- /// Writes UTF8 string at specific position (length-prefixed) and returns new position.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private int WriteStringAt(int pos, string value)
- {
- var byteCount = Utf8NoBom.GetByteCount(value);
- pos = WriteVarUIntAt(pos, (uint)byteCount);
- Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(pos, byteCount));
- return pos + byteCount;
- }
-
- // Remove old methods: WriteHeaderVarUInt, WriteHeaderString (no longer needed)
-
#endregion
#region Reference Handling
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs
index a1b5605..6d4f4b7 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinaryTypeMetadata.cs
@@ -1,23 +1,27 @@
using System;
-using System.Linq;
using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Text;
+using AyCode.Core.Serializers;
using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
- internal sealed class BinaryTypeMetadata : TypeMetadataBase
+ internal sealed class BinaryTypeMetadata : BinaryTypeMetadataBase
{
public BinaryPropertyAccessor[] Properties { get; }
public BinaryTypeMetadata(Type type) : base(type)
{
- Properties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false)
- .Select(p => new BinaryPropertyAccessor(p, type))
- .ToArray();
+ var orderedProperties = GetSerializableProperties(type, requiresRead: true, requiresWrite: false);
+
+ Properties = new BinaryPropertyAccessor[orderedProperties.Length];
+ for (var i = 0; i < orderedProperties.Length; i++)
+ {
+ var accessor = new BinaryPropertyAccessor(orderedProperties[i], type);
+ accessor.PropertyIndex = i;
+ Properties[i] = accessor;
+ }
}
}
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index 9f1161b..88a1306 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -26,6 +26,22 @@ namespace AyCode.Core.Serializers.Binaries;
public static partial class AcBinarySerializer
{
private static readonly ConcurrentDictionary TypeMetadataCache = new();
+
+ ///
+ /// ThreadLocal cache for hot path type metadata lookup.
+ /// Avoids ConcurrentDictionary overhead for frequently accessed types.
+ ///
+ [ThreadStatic]
+ private static Dictionary? t_typeMetadataLocalCache;
+
+ ///
+ /// Maximum local cache size before clearing.
+ /// Clear() is preferred over new Dictionary() because:
+ /// 1. Reuses already allocated internal array (no new allocation)
+ /// 2. Reduces GC pressure
+ /// 3. Adaptive: if cache grew to 64 once, it stays at that capacity
+ ///
+ private const int MaxLocalCacheSize = 64;
// Pre-computed UTF8 encoder for string operations
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
@@ -190,10 +206,8 @@ public static partial class AcBinarySerializer
ScanReferences(value, context, 0);
}
- if (options.UseMetadata && !IsPrimitiveOrStringFast(runtimeType))
- {
- RegisterMetadataForType(runtimeType, context);
- }
+ // Property index-based serialization - no metadata registration needed!
+ // PropertyIndex is deterministic (alphabetically ordered) and consistent across platforms
// Estimate and reserve header space to avoid body shift later
var estimatedHeaderSize = context.EstimateHeaderPayloadSize();
@@ -387,7 +401,7 @@ public static partial class AcBinarySerializer
///
/// Optimized primitive writer using TypeCode dispatch.
- /// Avoids Nullable.GetUnderlyingType in hot path by pre-computing in metadata.
+ /// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryWritePrimitive(object value, Type type, BinarySerializationContext context)
@@ -446,13 +460,6 @@ public static partial class AcBinarySerializer
return true;
}
- // Handle nullable types
- var underlyingType = Nullable.GetUnderlyingType(type);
- if (underlyingType != null)
- {
- return TryWritePrimitive(value, underlyingType, context);
- }
-
// Handle special types by reference comparison (faster than type equality)
if (ReferenceEquals(type, GuidType))
{
@@ -475,6 +482,15 @@ public static partial class AcBinarySerializer
return true;
}
+ // Handle nullable types - use cached check instead of GetUnderlyingType
+ // For nullable, value is already unwrapped when boxed, so we can use value.GetType()
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ // When boxed, nullable value types are unwrapped to their underlying type
+ // So we can just call TryWritePrimitive with the actual runtime type
+ return TryWritePrimitive(value, value.GetType(), context);
+ }
+
return false;
}
@@ -607,10 +623,10 @@ public static partial class AcBinarySerializer
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void WriteEnum(object value, BinarySerializationContext context
- )
+ private static void WriteEnum(object value, BinarySerializationContext context)
{
- var intValue = Convert.ToInt32(value);
+ // Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead
+ var intValue = GetEnumAsInt32Fast(value);
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
{
context.WriteByte(BinaryTypeCode.Enum);
@@ -622,6 +638,33 @@ public static partial class AcBinarySerializer
context.WriteVarInt(intValue);
}
+ ///
+ /// Fast enum to int conversion avoiding Convert.ToInt32 overhead.
+ /// Handles all common enum underlying types.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetEnumAsInt32Fast(object enumValue)
+ {
+ var type = enumValue.GetType();
+ var underlyingType = type.GetEnumUnderlyingType();
+
+ if (ReferenceEquals(underlyingType, IntType))
+ return (int)enumValue;
+ if (ReferenceEquals(underlyingType, typeof(byte)))
+ return (byte)enumValue;
+ if (ReferenceEquals(underlyingType, typeof(sbyte)))
+ return (sbyte)enumValue;
+ if (ReferenceEquals(underlyingType, typeof(short)))
+ return (short)enumValue;
+ if (ReferenceEquals(underlyingType, typeof(ushort)))
+ return (ushort)enumValue;
+ if (ReferenceEquals(underlyingType, typeof(uint)))
+ return unchecked((int)(uint)enumValue);
+
+ // Fallback for rare cases (long, ulong)
+ return Convert.ToInt32(enumValue);
+ }
+
///
/// Optimized string writer with FixStr for short strings.
/// Uses stackalloc for small strings to avoid allocations.
@@ -706,18 +749,10 @@ public static partial class AcBinarySerializer
if (IsPropertyDefaultOrNull(value, prop))
continue;
- // Write property name/index
- if (context.UseMetadata)
- {
- var propIndex = prop.CachedPropertyNameIndex >= 0
- ? prop.CachedPropertyNameIndex
- : context.GetPropertyNameIndex(prop.Name);
- context.WriteVarUInt((uint)propIndex);
- }
- else
- {
- context.WritePreencodedPropertyName(prop.NameUtf8);
- }
+ // Write property index directly - no dictionary lookup needed!
+ // PropertyIndex is deterministic (based on alphabetical ordering)
+ // and consistent across all platforms (WASM, iOS, Android, Windows)
+ context.WriteVarUInt((uint)prop.PropertyIndex);
// Write property value
WritePropertyValue(value, prop, context, nextDepth);
@@ -1059,9 +1094,41 @@ public static partial class AcBinarySerializer
return null;
}
+ ///
+ /// Gets type metadata with ThreadLocal caching for hot path optimization.
+ /// Falls back to ConcurrentDictionary for cache misses.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BinaryTypeMetadata GetTypeMetadata(Type type)
- => TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
+ {
+ // Fast path: check ThreadLocal cache first
+ var localCache = t_typeMetadataLocalCache;
+ if (localCache != null && localCache.TryGetValue(type, out var cached))
+ {
+ return cached;
+ }
+
+ // Slow path: get from global cache and populate local cache
+ return GetTypeMetadataSlow(type);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static BinaryTypeMetadata GetTypeMetadataSlow(Type type)
+ {
+ var metadata = TypeMetadataCache.GetOrAdd(type, static t => new BinaryTypeMetadata(t));
+
+ // Get or create local cache
+ var localCache = t_typeMetadataLocalCache ??= new Dictionary();
+
+ // Clear when full - reuses internal array, better than new Dictionary()
+ if (localCache.Count >= MaxLocalCacheSize)
+ {
+ localCache.Clear();
+ }
+
+ localCache[type] = metadata;
+ return metadata;
+ }
// Type metadata helpers moved to AcBinarySerializer.BinaryTypeMetadata.cs
diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
index db580f0..2f60f88 100644
--- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
+++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
@@ -16,6 +16,13 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
///
internal int CachedPropertyNameIndex = -1;
+ ///
+ /// Deterministic property index based on alphabetical ordering of property names.
+ /// This is computed once during metadata creation and is consistent across all platforms.
+ /// Used for fast serialization without dictionary lookup.
+ ///
+ public int PropertyIndex { get; internal set; } = -1;
+
///
/// The accessor type for fast typed getter dispatch.
///
diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs
new file mode 100644
index 0000000..c85bdbf
--- /dev/null
+++ b/AyCode.Core/Serializers/Binaries/BinaryTypeMetadataBase.cs
@@ -0,0 +1,41 @@
+using System.Reflection;
+using static AyCode.Core.Helpers.JsonUtilities;
+
+namespace AyCode.Core.Serializers.Binaries;
+
+///
+/// Base class for binary serializer/deserializer type metadata.
+/// Provides common functionality for IId detection and property ordering.
+/// Properties are ordered alphabetically by name for deterministic serialization across platforms.
+///
+public abstract class BinaryTypeMetadataBase : TypeMetadataBase
+{
+ ///
+ /// Whether this type implements IId interface.
+ ///
+ public bool IsIId { get; }
+
+ ///
+ /// The Id property type if IsIId is true.
+ ///
+ public Type? IdType { get; }
+
+ ///
+ /// Compiled getter for the Id property (if IsIId is true).
+ ///
+ public Func? IdGetter { get; }
+
+ protected BinaryTypeMetadataBase(Type type) : base(type)
+ {
+ var (isIId, idType) = GetIdInfo(type);
+ IsIId = isIId;
+ IdType = idType;
+
+ if (isIId)
+ {
+ var idProp = type.GetProperty("Id");
+ if (idProp != null)
+ IdGetter = AcSerializerCommon.CreateCompiledGetter(type, idProp);
+ }
+ }
+}
diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs
index b525169..ac31576 100644
--- a/AyCode.Core/Serializers/TypeMetadataBase.cs
+++ b/AyCode.Core/Serializers/TypeMetadataBase.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using System.Reflection;
using static AyCode.Core.Helpers.JsonUtilities;
@@ -9,6 +10,13 @@ namespace AyCode.Core.Serializers;
///
public abstract class TypeMetadataBase
{
+ ///
+ /// Cache for serializable properties per type.
+ /// Key: (Type, requiresWrite) - since requiresRead is always true in practice.
+ /// Value: PropertyInfo[] ordered alphabetically by name for deterministic serialization.
+ ///
+ private static readonly ConcurrentDictionary<(Type, bool), PropertyInfo[]> OrderedPropertiesCache = new();
+
///
/// Compiled parameterless constructor for the type.
/// Null if the type is abstract or has no parameterless constructor.
@@ -21,21 +29,29 @@ public abstract class TypeMetadataBase
}
///
- /// Gets the properties that should be serialized for a type.
+ /// Gets serializable properties ordered alphabetically by name.
+ /// Results are cached per type and requiresWrite combination.
+ /// Ordering ensures deterministic serialization across all platforms (Windows, WASM, iOS, Android).
///
/// The type to analyze.
- /// Whether the property must be readable.
- /// Whether the property must be writable.
- /// Enumerable of properties that meet the criteria.
- protected static IEnumerable GetSerializableProperties(
+ /// Whether the property must be readable (always true in practice).
+ /// Whether the property must be writable (true for deserialization).
+ /// Array of properties ordered by name (cached).
+ protected static PropertyInfo[] GetSerializableProperties(
Type type,
bool requiresRead = true,
bool requiresWrite = false)
{
- return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
- .Where(p => (!requiresRead || p.CanRead) &&
- (!requiresWrite || p.CanWrite) &&
- p.GetIndexParameters().Length == 0 &&
- !HasJsonIgnoreAttribute(p));
+ return OrderedPropertiesCache.GetOrAdd((type, requiresWrite), static key =>
+ {
+ var (t, needsWrite) = key;
+ return t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(p => p.CanRead &&
+ (!needsWrite || p.CanWrite) &&
+ p.GetIndexParameters().Length == 0 &&
+ !HasJsonIgnoreAttribute(p))
+ .OrderBy(static p => p.Name, StringComparer.Ordinal)
+ .ToArray();
+ });
}
}