Polymorphic serialization: slot-based prefix system overhaul

Refactored BinaryTypeCode to reserve 0..63 for FixObj slot indices, enabling direct array access for object wrappers. Introduced a new polymorphic type prefix system for properties whose runtime type differs from their declared type, with first/repeated occurrence markers and combined ref-tracking support. Unified wrapper slot caching for SGen and runtime types, improving performance and eliminating dictionary lookups in hot paths. Updated code generation, tests, and constants to use the new slot system. Added new settings and utility scripts. Overall, serialization is now faster, more robust, and extensible.
This commit is contained in:
Loretta 2026-03-09 15:04:46 +01:00
parent 11a15bfa64
commit 68c25b2381
12 changed files with 512 additions and 169 deletions

View File

@ -44,7 +44,9 @@
"Bash(curl -s \"https://api.github.com/repos/Cysharp/MemoryPack/git/trees/main?recursive=1\")", "Bash(curl -s \"https://api.github.com/repos/Cysharp/MemoryPack/git/trees/main?recursive=1\")",
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs\")", "Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs\")",
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Core/MemoryPackCode.cs\")", "Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Core/MemoryPackCode.cs\")",
"Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs\")" "Bash(curl -sL \"https://raw.githubusercontent.com/Cysharp/MemoryPack/main/src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs\")",
"Bash(perl -i -pe 's/GetWrapperBySlot\\\\\\(\\([^,]+\\), \\(typeof\\\\\\([^\\)]+\\\\\\)\\)\\\\\\)/GetWrapper\\($2, $1\\)/g' \"H:/Applications/Aycode/Source/AyCode.Core/AyCode.Core.Serializers.SourceGenerator/AcBinarySourceGenerator.cs\")",
"Bash(wc -l H:/Applications/Aycode/Source/AyCode.Core/AyCode.Core/Serializers/Binaries/*.cs)"
] ]
} }
} }

View File

@ -36,12 +36,14 @@ public static class Program
// Serializer name constants // Serializer name constants
private const string SerializerMessagePack = "MessagePack"; private const string SerializerMessagePack = "MessagePack";
private const string SerializerAcBinaryDefault = "AcBinary (Default)"; private const string SerializerAcBinaryDefault = "AcBinary (Default)";
private const string SerializerAcBinaryDefaultNoSgen = "AcBinary (Def, NoSgen)";
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)"; private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)"; private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
private const string SerializerAcBinaryFastNoSgen = "AcBinary (Fast, NoSgen)";
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)"; private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
private const string SerializerMemoryPack = "MemoryPack"; private const string SerializerMemoryPack = "MemoryPack";
private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)"; //private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)";
private const string SerializerSystemTextJson = "System.Text.Json"; //private const string SerializerSystemTextJson = "System.Text.Json";
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
@ -208,20 +210,34 @@ public static class Program
private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData) private static List<ISerializerBenchmark> CreateSerializers(TestDataSet testData)
{ {
var binaryNoInternOption = AcBinarySerializerOptions.Default;
binaryNoInternOption.UseStringInterning = StringInterningMode.None;
var binaryDefaultNoSgenOption = AcBinarySerializerOptions.Default;
binaryDefaultNoSgenOption.UseGeneratedCode = false;
var binaryFastModeNoSgenOption = AcBinarySerializerOptions.FastMode;
binaryFastModeNoSgenOption.UseGeneratedCode = false;
return new List<ISerializerBenchmark> return new List<ISerializerBenchmark>
{ {
// AcBinary variants // AcBinary variants
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern), new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, SerializerAcBinaryFastNoSgen),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
new AcBinaryBenchmark(testData.Order, binaryDefaultNoSgenOption, SerializerAcBinaryDefaultNoSgen),
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
new AcBinaryBenchmark(testData.Order, binaryNoInternOption, SerializerAcBinaryNoIntern),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastNoSgen),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefaultNoSgen),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern), //new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
// MemoryPack // MemoryPack
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack), new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
@ -229,10 +245,10 @@ public static class Program
new MessagePackBenchmark(testData.Order, SerializerMessagePack), new MessagePackBenchmark(testData.Order, SerializerMessagePack),
// AcBinary BufferWriter // AcBinary BufferWriter
new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter), //new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
// System.Text.Json // System.Text.Json
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson) //new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
}; };
} }

View File

@ -583,7 +583,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(); sb.AppendLine();
sb.AppendLine(" if (context.HasRefHandling)"); sb.AppendLine(" if (context.HasRefHandling)");
sb.AppendLine(" {"); sb.AppendLine(" {");
sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); sb.AppendLine($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
sb.AppendLine(" {"); sb.AppendLine(" {");
@ -600,7 +600,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
sb.AppendLine(); sb.AppendLine();
sb.AppendLine(" if (context.HasAllRefHandling)"); sb.AppendLine(" if (context.HasAllRefHandling)");
sb.AppendLine(" {"); sb.AppendLine(" {");
sb.AppendLine($" var wrapper = context.GetWrapperBySlot(s_wrapperSlot, typeof({ci.FullTypeName}));"); sb.AppendLine($" var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;"); sb.AppendLine(" var visitIndex = context.ScanVisitIndex++;");
sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))"); sb.AppendLine(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
sb.AppendLine(" {"); sb.AppendLine(" {");
@ -1227,7 +1227,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
// No ref, but metadata possible → UseMetadata branch only // No ref, but metadata possible → UseMetadata branch only
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.TypeNameForTypeof}), {writer}.s_wrapperSlot));");
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
@ -1267,7 +1267,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
// Full path: ref tracking + metadata // Full path: ref tracking + metadata
sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.TypeNameForTypeof})));"); sb.AppendLine($"{i} var isFirstMeta_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.TypeNameForTypeof}), {writer}.s_wrapperSlot));");
var refGuard = p.IsIId var refGuard = p.IsIId
? "context.HasRefHandling" ? "context.HasRefHandling"
: "context.HasAllRefHandling"; : "context.HasAllRefHandling";
@ -1398,7 +1398,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
// No ref, but metadata possible → UseMetadata branch only // No ref, but metadata possible → UseMetadata branch only
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.ElementFullTypeName}), {writer}.s_wrapperSlot));");
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
@ -1442,7 +1442,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
// Full path: ref tracking + metadata // Full path: ref tracking + metadata
sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({p.ElementFullTypeName})));"); sb.AppendLine($"{i} var isFirstMeta_e_{p.Name} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({p.ElementFullTypeName}), {writer}.s_wrapperSlot));");
sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))"); sb.AppendLine($"{i} if ({elemRefGuard} && context.TryConsumeWritePlanEntry(out var epe_{p.Name}))");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)"); sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
@ -1655,7 +1655,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
// No ref, metadata possible // No ref, metadata possible
sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({valType})));"); sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({valType}), {writer}.s_wrapperSlot));");
sb.AppendLine($"{i} if (context.UseMetadata)"); sb.AppendLine($"{i} if (context.UseMetadata)");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);"); sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
@ -1698,7 +1698,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
else else
{ {
// Full path: ref tracking + metadata // Full path: ref tracking + metadata
sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapperBySlot({writer}.s_wrapperSlot, typeof({valType})));"); sb.AppendLine($"{i} var isFirstMeta_dv_{s} = context.UseMetadata && AcBinarySerializer.BinarySerializationContext<TOutput>.RegisterMetadataType(context.GetWrapper(typeof({valType}), {writer}.s_wrapperSlot));");
sb.AppendLine($"{i} if ({dvRefGuard} && context.TryConsumeWritePlanEntry(out var dpe_{s}))"); sb.AppendLine($"{i} if ({dvRefGuard} && context.TryConsumeWritePlanEntry(out var dpe_{s}))");
sb.AppendLine($"{i} {{"); sb.AppendLine($"{i} {{");
sb.AppendLine($"{i} if (!dpe_{s}.IsFirst)"); sb.AppendLine($"{i} if (!dpe_{s}.IsFirst)");

View File

@ -13,7 +13,7 @@ public class AcBinarySerializerBasicTests
{ {
var result = AcBinarySerializer.Serialize<object?>(null); var result = AcBinarySerializer.Serialize<object?>(null);
Assert.AreEqual(1, result.Length); Assert.AreEqual(1, result.Length);
Assert.AreEqual((byte)0, result[0]); Assert.AreEqual(BinaryTypeCode.Null, result[0]);
} }
[TestMethod] [TestMethod]

View File

@ -55,8 +55,9 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
private readonly Dictionary<Type, TypeMetadataWrapper<TMetadata>> _wrappers = new(); private readonly Dictionary<Type, TypeMetadataWrapper<TMetadata>> _wrappers = new();
/// <summary> /// <summary>
/// Slot-indexed wrapper cache for SGen types. Indexed by AllocateWrapperSlot() value. /// Slot-indexed wrapper cache. Shared between SGen types (RuntimeSlotCount+) and
/// Avoids dictionary lookup — direct array access for types with compile-time known slot index. /// runtime polymorphic type cache (0..RuntimeSlotCount-1).
/// Avoids dictionary lookup — direct array access (~1-2ns vs ~15-25ns).
/// Not cleared on pool return: wrapper references are stable across serialization calls. /// Not cleared on pool return: wrapper references are stable across serialization calls.
/// </summary> /// </summary>
private TypeMetadataWrapper<TMetadata>?[]? _wrapperSlots; private TypeMetadataWrapper<TMetadata>?[]? _wrapperSlots;
@ -107,33 +108,35 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
} }
/// <summary> /// <summary>
/// Gets or creates a wrapper for the specified type using a pre-allocated slot index. /// Gets or creates a wrapper for the specified type using a slot index.
/// SGen types call this with their compile-time known slot — avoids dictionary lookup. /// Slot checked first (array access ~1-2ns), falls back to dictionary if slot empty.
/// First call per slot per context: falls back to GetWrapper + stores in slot array. /// Used by both SGen (compile-time slot) and runtime polymorphic cache (0..RuntimeSlotCount-1).
/// Subsequent calls: direct array index (~1-2ns vs ~15-25ns dictionary lookup).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public TypeMetadataWrapper<TMetadata> GetWrapperBySlot(int slot, Type type) public TypeMetadataWrapper<TMetadata> GetWrapper(Type type, int slotIndex)
{ {
var slots = _wrapperSlots; var slots = _wrapperSlots!;
if (slots != null && slot < slots.Length) var wrapper = slots[slotIndex];
{ if (wrapper != null)
var wrapper = slots[slot]; return wrapper;
if (wrapper != null)
return wrapper;
}
return GetWrapperBySlotSlow(slot, type); wrapper = GetWrapper(type);
} slots[slotIndex] = wrapper;
[MethodImpl(MethodImplOptions.NoInlining)]
private TypeMetadataWrapper<TMetadata> GetWrapperBySlotSlow(int slot, Type type)
{
var wrapper = GetWrapper(type);
_wrapperSlots![slot] = wrapper;
return wrapper; return wrapper;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected TypeMetadataWrapper<TMetadata>? GetWrapperSlot(int slotIndex)
=> _wrapperSlots![slotIndex];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void SetWrapperSlot(int slotIndex, TypeMetadataWrapper<TMetadata> wrapper)
=> _wrapperSlots![slotIndex] = wrapper;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void ClearWrapperSlots(int count)
=> Array.Clear(_wrapperSlots!, 0, count);
/// <summary> /// <summary>
/// Pre-allocates the wrapper slot array with the known total slot count. /// Pre-allocates the wrapper slot array with the known total slot count.
/// Called once from derived context constructor after all AllocateWrapperSlot() calls have completed. /// Called once from derived context constructor after all AllocateWrapperSlot() calls have completed.

View File

@ -3,6 +3,7 @@ using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;
@ -104,6 +105,19 @@ public static partial class AcBinaryDeserializer
private MetadataEntry[]? _metadataEntries; private MetadataEntry[]? _metadataEntries;
private int _metadataEntryCount; private int _metadataEntryCount;
/// <summary>
/// Runtime polymorphic slot counter. Indices 0..RuntimeSlotCount-1 stored in _wrapperSlots.
/// Overflow (index >= RuntimeSlotCount) stored in _polyOverflow.
/// </summary>
internal int _nextRuntimeSlot;
/// <summary>
/// Overflow array for polymorphic types beyond RuntimeSlotCount.
/// Only allocated when >RuntimeSlotCount distinct poly types (very rare).
/// Indexed by (polyIndex - RuntimeSlotCount).
/// </summary>
private TypeMetadataWrapper<BinaryDeserializeTypeMetadata>[]? _polyOverflow;
/// <summary> /// <summary>
/// A metadata entry for the deserializer. /// A metadata entry for the deserializer.
/// </summary> /// </summary>
@ -121,6 +135,7 @@ public static partial class AcBinaryDeserializer
public BinaryDeserializationContext() public BinaryDeserializationContext()
{ {
InitializeWrapperSlots(Volatile.Read(ref AcBinarySerializer.s_nextWrapperSlot));
} }
/// <summary> /// <summary>
@ -367,6 +382,54 @@ public static partial class AcBinaryDeserializer
#endregion #endregion
#region Polymorphic Wrapper Cache
/// <summary>
/// Registers a wrapper in the polymorphic cache (called by ReadObjectWithTypeName/RefFirst).
/// Indices 0..RuntimeSlotCount-1 → _wrapperSlots (fast path, ~1-2ns).
/// Indices RuntimeSlotCount+ → _polyOverflow (still O(1) array access).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RegisterPolymorphicWrapper(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var slot = _nextRuntimeSlot++;
if (slot < AcBinarySerializer.RuntimeSlotCount)
{
SetWrapperSlot(slot, wrapper);
}
else
{
RegisterPolymorphicWrapperOverflow(wrapper, slot - AcBinarySerializer.RuntimeSlotCount);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void RegisterPolymorphicWrapperOverflow(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int overflowIndex)
{
if (_polyOverflow == null || overflowIndex >= _polyOverflow.Length)
{
var newSize = _polyOverflow == null ? 4 : _polyOverflow.Length * 2;
var newArray = new TypeMetadataWrapper<BinaryDeserializeTypeMetadata>[newSize];
if (_polyOverflow != null)
Array.Copy(_polyOverflow, newArray, overflowIndex);
_polyOverflow = newArray;
}
_polyOverflow[overflowIndex] = wrapper;
}
/// <summary>
/// Gets a previously registered polymorphic wrapper by index (called by ReadObjectWithTypeIndex/RefFirst).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TypeMetadataWrapper<BinaryDeserializeTypeMetadata> GetPolymorphicWrapper(int index)
{
if (index < AcBinarySerializer.RuntimeSlotCount)
return GetWrapperSlot(index)!;
return _polyOverflow![index - AcBinarySerializer.RuntimeSlotCount]!;
}
#endregion
#region Reset & Clear #region Reset & Clear
public override void Reset(AcBinarySerializerOptions options) public override void Reset(AcBinarySerializerOptions options)
@ -381,6 +444,17 @@ public static partial class AcBinaryDeserializer
_metadataEntryCount = 0; _metadataEntryCount = 0;
_nextCacheIndex = 0; _nextCacheIndex = 0;
// Clear runtime FixObj slots to prevent stale wrapper reuse on pool return.
// Slot-to-type mapping changes between sessions (slot 0 may be TestOrder in session A
// but TestGenericAttribute in session B). Without clearing, ReadObjectFromSlot
// would reuse the stale wrapper → wrong type → stream misalignment.
if (_nextRuntimeSlot > 0)
{
ClearWrapperSlots(Math.Min(_nextRuntimeSlot, AcBinarySerializer.RuntimeSlotCount));
}
_nextRuntimeSlot = 0;
// String cache: clear content but keep dictionary allocated for reuse // String cache: clear content but keep dictionary allocated for reuse
_stringCache?.Clear(); _stringCache?.Clear();

View File

@ -102,17 +102,26 @@ public static partial class AcBinaryDeserializer
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst; readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef; readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName; readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst;
readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex;
readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst;
readers[BinaryTypeCode.Array] = ReadArray; readers[BinaryTypeCode.Array] = ReadArray;
readers[BinaryTypeCode.Dictionary] = ReadDictionary; readers[BinaryTypeCode.Dictionary] = ReadDictionary;
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx); readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
// Register FixStr readers (34-65) // Register FixStr readers
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++) for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
{ {
var length = BinaryTypeCode.DecodeFixStrLength(code); var length = BinaryTypeCode.DecodeFixStrLength(code);
readers[code] = CreateFixStrReader<TInput>(length); readers[code] = CreateFixStrReader<TInput>(length);
} }
// Register FixObj slot readers (0..SlotCount-1)
for (int slot = 0; slot < BinaryTypeCode.SlotCount; slot++)
{
readers[slot] = CreateFixObjReader<TInput>(slot);
}
return readers; return readers;
} }
} }
@ -131,6 +140,16 @@ public static partial class AcBinaryDeserializer
return (ctx, _, _) => ctx.ReadStringUtf8(length); return (ctx, _, _) => ctx.ReadStringUtf8(length);
} }
/// <summary>
/// Creates a reader for FixObj slot (0..SlotCount-1).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TypeReader<TInput> CreateFixObjReader<TInput>(int slot)
where TInput : struct, IBinaryInputBase
{
return (ctx, targetType, depth) => ReadObjectFromSlot(ctx, slot, targetType, depth);
}
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
#region Public API #region Public API
@ -908,16 +927,16 @@ public static partial class AcBinaryDeserializer
var typeCode = context.ReadByte(); var typeCode = context.ReadByte();
// Handle null first // Handle tiny int first (most common case for small integers, >= 192)
if (typeCode == BinaryTypeCode.Null) return null;
// Handle tiny int (most common case for small integers)
if (BinaryTypeCode.IsTinyInt(typeCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode); var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
return ConvertToTargetType(intValue, targetType); return ConvertToTargetType(intValue, targetType);
} }
// Handle null
if (typeCode == BinaryTypeCode.Null) return null;
// Handle FixStr (short strings with length in type code) // Handle FixStr (short strings with length in type code)
if (BinaryTypeCode.IsFixStr(typeCode)) if (BinaryTypeCode.IsFixStr(typeCode))
{ {
@ -1124,6 +1143,36 @@ public static partial class AcBinaryDeserializer
return context.GetInternedObject(cacheIndex); return context.GetInternedObject(cacheIndex);
} }
/// <summary>
/// FixObj slot read: marker byte (0..SlotCount-1) is the slot index.
/// First occurrence: wrapper is null in slot → resolve from targetType, cache in slot.
/// Subsequent: direct array access (~1-2ns).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectFromSlot<TInput>(
BinaryDeserializationContext<TInput> context,
int slot,
Type targetType,
int depth)
where TInput : struct, IBinaryInputBase
{
var wrapper = context.GetWrapper(targetType, slot);
// Track highest slot used for Clear()
if (slot >= context._nextRuntimeSlot)
context._nextRuntimeSlot = slot + 1;
// SGen fast path (same as ReadObjectCore)
if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
{
var generatedReader = wrapper.GeneratedReader;
if (generatedReader != null)
return generatedReader.ReadObject(context, depth, cacheIndex: -1);
}
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex: -1);
}
/// <summary> /// <summary>
/// Object olvasása (nem tracked, vagy UseMetadata nélkül). /// Object olvasása (nem tracked, vagy UseMetadata nélkül).
/// Wire format: [Object][props...] /// Wire format: [Object][props...]
@ -1147,10 +1196,10 @@ public static partial class AcBinaryDeserializer
} }
/// <summary> /// <summary>
/// Polymorphic object prefix: declared property type is System.Object. /// Polymorphic PREFIX marker: declared type ≠ runtime type.
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [Object (25) or ObjectRefFirst (66) ...] [props...] /// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...]
/// Reads the runtime type name, resolves it, then delegates to ReadValue with the resolved type /// Reads the runtime type name, resolves it, registers wrapper in poly slot cache,
/// so the next marker (Object/ObjectRefFirst/etc.) is processed normally. /// then reads the inner marker via ReadValue.
/// </summary> /// </summary>
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth) private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase where TInput : struct, IBinaryInputBase
@ -1160,10 +1209,59 @@ public static partial class AcBinaryDeserializer
?? throw new AcBinaryDeserializationException( ?? throw new AcBinaryDeserializationException(
$"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.", $"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
context.Position, null); context.Position, null);
// Next byte is the actual object marker (Object/ObjectRefFirst/etc.) — read it via ReadValue var wrapper = context.GetWrapper(resolvedType);
context.RegisterPolymorphicWrapper(wrapper);
// Next byte is the actual inner marker (Object/Array/Dict/etc.) — read it via ReadValue
return ReadValue(context, resolvedType, depth); return ReadValue(context, resolvedType, depth);
} }
/// <summary>
/// Polymorphic COMBINED marker: first type occurrence + ref tracking first occurrence.
/// Wire format: [ObjectWithTypeNameRefFirst (69)] [TypeName string] [VarUInt refCacheIndex] [properties...]
/// Object body follows directly — no inner Object/ObjectRefFirst marker.
/// </summary>
private static object? ReadObjectWithTypeNameRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeName = ReadPlainString(context);
var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
?? throw new AcBinaryDeserializationException(
$"Cannot resolve type '{typeName}' for ObjectWithTypeNameRefFirst at position {context.Position}.",
context.Position, null);
var wrapper = context.GetWrapper(resolvedType);
context.RegisterPolymorphicWrapper(wrapper);
var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
/// <summary>
/// Polymorphic PREFIX marker with cached type index.
/// Wire format: [ObjectWithTypeIndex (70)] [VarUInt typeIndex] [inner marker: Object/Array/Dict/...] [body...]
/// Looks up the previously registered wrapper by index (~1-2ns array access),
/// then reads the inner marker via ReadValue.
/// </summary>
private static object? ReadObjectWithTypeIndex<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeIndex = (int)context.ReadVarUInt();
var wrapper = context.GetPolymorphicWrapper(typeIndex);
return ReadValue(context, wrapper.Metadata.SourceType, depth);
}
/// <summary>
/// Polymorphic COMBINED marker: cached type index + ref tracking first occurrence.
/// Wire format: [ObjectWithTypeIndexRefFirst (71)] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
/// Object body follows directly — no inner Object/ObjectRefFirst marker. 0 dictionary lookup.
/// </summary>
private static object? ReadObjectWithTypeIndexRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
where TInput : struct, IBinaryInputBase
{
var typeIndex = (int)context.ReadVarUInt();
var wrapper = context.GetPolymorphicWrapper(typeIndex);
var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectCoreWithWrapper(context, wrapper, depth, cacheIndex);
}
/// <summary> /// <summary>
/// Object olvasás core implementáció. /// Object olvasás core implementáció.
/// </summary> /// </summary>

View File

@ -160,6 +160,12 @@ public static partial class AcBinarySerializer
/// </summary> /// </summary>
internal bool StringInternEligible; internal bool StringInternEligible;
/// <summary>
/// Next polymorphic type cache index. Assigned sequentially on first polymorphic write per runtime type.
/// Used together with TypeMetadataWrapper.PolymorphicSeen/PolymorphicCacheIndex.
/// </summary>
internal int _nextTypeSlot;
/// <summary> /// <summary>
/// Tries to consume the next write plan entry at the current WriteVisitIndex. /// Tries to consume the next write plan entry at the current WriteVisitIndex.
/// Returns true if the entry matches (duplicate exists at this visit point). /// Returns true if the entry matches (duplicate exists at this visit point).
@ -301,6 +307,7 @@ public static partial class AcBinarySerializer
WriteVisitIndex = 0; WriteVisitIndex = 0;
_nextWritePlanVisitIndex = int.MaxValue; _nextWritePlanVisitIndex = int.MaxValue;
StringInternEligible = false; StringInternEligible = false;
_nextTypeSlot = 0;
// Clear write plan string references to avoid GC pinning, keep array if small enough // Clear write plan string references to avoid GC pinning, keep array if small enough
if (_writePlan != null) if (_writePlan != null)
@ -906,6 +913,61 @@ public static partial class AcBinarySerializer
#endregion #endregion
#region Polymorphic Type Prefix
/// <summary>
/// Writes a polymorphic type prefix when the runtime type differs from the declared property type.
/// <para>
/// When <paramref name="cachedObjectCacheIndex"/> is -1 (default): PREFIX markers.
/// An inner Object/Array/Dict marker follows.
/// First type occurrence: ObjectWithTypeName (68) + typename
/// Cached type: ObjectWithTypeIndex (70) + typeIndex
/// </para>
/// <para>
/// When <paramref name="cachedObjectCacheIndex"/> >= 0: COMBINED markers.
/// Object body follows directly (no inner Object/ObjectRefFirst marker).
/// First type occurrence: ObjectWithTypeNameRefFirst (69) + typename + refCacheIndex
/// Cached type: ObjectWithTypeIndexRefFirst (71) + typeIndex + refCacheIndex
/// </para>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WritePolymorphicPrefix(Type runtimeType, int cachedObjectCacheIndex = -1)
{
var rtWrapper = GetWrapper(runtimeType);
if (!rtWrapper.PolymorphicSeen)
{
rtWrapper.PolymorphicSeen = true;
rtWrapper.PolymorphicCacheIndex = _nextTypeSlot++;
if (cachedObjectCacheIndex >= 0)
{
WriteByte(BinaryTypeCode.ObjectWithTypeNameRefFirst);
WriteStringUtf8(runtimeType.FullName!);
WriteVarUInt((uint)cachedObjectCacheIndex);
}
else
{
WriteByte(BinaryTypeCode.ObjectWithTypeName);
WriteStringUtf8(runtimeType.FullName!);
}
}
else
{
if (cachedObjectCacheIndex >= 0)
{
WriteByte(BinaryTypeCode.ObjectWithTypeIndexRefFirst);
WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
WriteVarUInt((uint)cachedObjectCacheIndex);
}
else
{
WriteByte(BinaryTypeCode.ObjectWithTypeIndex);
WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
}
}
}
#endregion
#region UseMetadata Type Tracking #region UseMetadata Type Tracking
/// <summary> /// <summary>

View File

@ -248,12 +248,22 @@ public static partial class AcBinarySerializer
#region SGen Slot Allocation #region SGen Slot Allocation
private static int s_nextWrapperSlot; /// <summary>
/// Number of runtime wrapper slots reserved for polymorphic type cache (indices 0..RuntimeSlotCount-1).
/// SGen compile-time slots start at RuntimeSlotCount and above.
/// Easily modifiable — all code references this constant instead of literal values.
/// </summary>
internal const int RuntimeSlotCount = BinaryTypeCode.SlotCount;
/// <summary>
/// Next available wrapper slot index. Starts at RuntimeSlotCount so SGen slots
/// don't collide with runtime polymorphic slots (0..RuntimeSlotCount-1).
/// </summary>
internal static int s_nextWrapperSlot = RuntimeSlotCount + 1;
/// <summary> /// <summary>
/// Allocates a unique slot index for SGen wrapper cache. /// Allocates a unique slot index for SGen wrapper cache.
/// Indexes _wrapperSlots array on AcSerializerContextBase. /// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ...
/// Used for: IdentityMap ref tracking (scan pass), MetadataSeen (write pass).
/// </summary> /// </summary>
internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1; internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;
@ -574,7 +584,7 @@ public static partial class AcBinarySerializer
return; return;
} }
var wrapper = context.GetWrapperBySlot(wrapperSlot, type); var wrapper = context.GetWrapper(type, wrapperSlot);
WriteObject(value, wrapper, context, depth); WriteObject(value, wrapper, context, depth);
} }
@ -651,8 +661,9 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache). /// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects. /// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
/// When polyRuntimeType is set, writes polymorphic prefix/combined markers.
/// </summary> /// </summary>
private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth) private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type? polyRuntimeType = null)
where TOutput : struct, IBinaryOutputBase where TOutput : struct, IBinaryOutputBase
{ {
var type = wrapper.Metadata.SourceType; var type = wrapper.Metadata.SourceType;
@ -666,6 +677,7 @@ public static partial class AcBinarySerializer
if (depth > context.MaxDepth) if (depth > context.MaxDepth)
{ {
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
context.WriteByte(BinaryTypeCode.Null); context.WriteByte(BinaryTypeCode.Null);
return; return;
} }
@ -673,6 +685,7 @@ public static partial class AcBinarySerializer
// Handle byte arrays specially (value-like, no reference tracking) // Handle byte arrays specially (value-like, no reference tracking)
if (value is byte[] byteArray) if (value is byte[] byteArray)
{ {
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
WriteByteArray(byteArray, context); WriteByteArray(byteArray, context);
return; return;
} }
@ -680,6 +693,7 @@ public static partial class AcBinarySerializer
// Handle dictionaries // Handle dictionaries
if (value is IDictionary dictionary) if (value is IDictionary dictionary)
{ {
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
WriteDictionary(dictionary, context, depth); WriteDictionary(dictionary, context, depth);
return; return;
} }
@ -687,12 +701,13 @@ public static partial class AcBinarySerializer
// Handle collections/arrays // Handle collections/arrays
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType)) if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
{ {
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
WriteArray(enumerable, wrapper, context, depth); WriteArray(enumerable, wrapper, context, depth);
return; return;
} }
// Handle complex objects with single-pass reference tracking // Handle complex objects — combined poly+ref markers handled inside WriteObject
WriteObject(value, wrapper, context, depth); WriteObject(value, wrapper, context, depth, polyRuntimeType);
} }
/// <summary> /// <summary>
@ -1058,7 +1073,7 @@ public static partial class AcBinarySerializer
#region Complex Type Writers #region Complex Type Writers
private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth) private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth, Type? polyRuntimeType = null)
where TOutput : struct, IBinaryOutputBase where TOutput : struct, IBinaryOutputBase
{ {
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
@ -1089,7 +1104,8 @@ public static partial class AcBinarySerializer
} }
else else
{ {
// 2+ occurrence → write ObjectRef (no children, no properties) // 2+ occurrence → write ObjectRef directly (no poly prefix needed —
// object already in cache, deser knows the type)
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarUInt((uint)planEntry.CacheMapIndex); context.WriteVarUInt((uint)planEntry.CacheMapIndex);
return; return;
@ -1097,10 +1113,12 @@ public static partial class AcBinarySerializer
} }
} }
// Marker kiírása: // Marker kiírása — polymorphic vs non-polymorphic paths
// - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex if (polyRuntimeType != null)
// - Non-cached: Object/ObjectWithMetadata {
if (useMetaForType) WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
}
else if (useMetaForType)
{ {
if (cachedObjectCacheIndex >= 0) if (cachedObjectCacheIndex >= 0)
{ {
@ -1122,7 +1140,16 @@ public static partial class AcBinarySerializer
} }
else else
{ {
context.WriteByte(BinaryTypeCode.Object); // FixObj: assign slot on first occurrence this session
if (!wrapper.PolymorphicSeen)
{
wrapper.PolymorphicSeen = true;
wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
}
if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
else
context.WriteByte(BinaryTypeCode.Object);
} }
} }
@ -1191,6 +1218,39 @@ public static partial class AcBinarySerializer
} }
} }
/// <summary>
/// Polymorphic marker writing — extracted from WriteObject to keep hot path small.
/// Cold path: polymorphism is rare, NoInlining call overhead acceptable.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WritePolymorphicMarker<TOutput>(
BinarySerializationContext<TOutput> context,
Type polyRuntimeType,
int cachedObjectCacheIndex)
where TOutput : struct, IBinaryOutputBase
{
if (cachedObjectCacheIndex >= 0)
{
// Combined poly + RefFirst marker (69/71)
context.WritePolymorphicPrefix(polyRuntimeType, cachedObjectCacheIndex);
}
else
{
var rtWrapper = context.GetWrapper(polyRuntimeType);
if (rtWrapper.PolymorphicSeen && rtWrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
{
// 2+ poly in this session → FixObj (1 byte)
context.WriteByte((byte)rtWrapper.PolymorphicCacheIndex);
}
else
{
// First poly in this session → ObjectWithTypeName + assigns slot
context.WritePolymorphicPrefix(polyRuntimeType);
context.WriteByte(BinaryTypeCode.Object);
}
}
}
/// <summary> /// <summary>
/// Checks if a property value is null or default without boxing for value types. /// Checks if a property value is null or default without boxing for value types.
/// </summary> /// </summary>
@ -1500,14 +1560,11 @@ public static partial class AcBinarySerializer
{ {
var runtimeType = value.GetType(); var runtimeType = value.GetType();
// System.Object declared property → prefix with ObjectWithTypeName marker + TypeName // Polymorphic detection: when declared type ≠ runtime type, pass polyRuntimeType
// so the deserializer can resolve the concrete runtime type. // to WriteValueNonPrimitiveWithWrapper → WriteObject for combined marker handling.
// The normal Object/ObjectRefFirst marker follows as usual. // For collections: normal prefix pattern (68/70 + inner Array/Dict marker).
if (prop.IsObjectDeclaredType && !context.UseMetadata) // For objects: combined markers (69/71) when RefFirst, no prefix for ObjectRef.
{ var isPoly = !context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType;
context.WriteByte(BinaryTypeCode.ObjectWithTypeName);
context.WriteStringUtf8(runtimeType.AssemblyQualifiedName!);
}
var complexIdx = prop.ComplexPropertyIndex; var complexIdx = prop.ComplexPropertyIndex;
if (complexIdx >= 0) if (complexIdx >= 0)
@ -1518,11 +1575,12 @@ public static partial class AcBinarySerializer
propWrapper = context.GetWrapper(runtimeType); propWrapper = context.GetWrapper(runtimeType);
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper); parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
} }
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth); WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null);
} }
else else
{ {
// Non-complex in default case (nullable value type, etc.) // Non-complex in default case (nullable value type, etc.)
if (isPoly) context.WritePolymorphicPrefix(runtimeType);
WriteValueNonPrimitive(value, runtimeType, context, depth); WriteValueNonPrimitive(value, runtimeType, context, depth);
} }
} }

View File

@ -34,6 +34,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
/// </summary> /// </summary>
public bool IsObjectDeclaredType { get; } public bool IsObjectDeclaredType { get; }
/// <summary>
/// True when declared property type is a non-sealed reference type (not string).
/// Polymorphism is possible: runtime type may differ from declared type.
/// Covers object, interface, abstract, and non-sealed class properties.
/// When true, serializer checks GetType() != PropertyType and writes polymorphic prefix.
/// When false (sealed, value type, string): 0 overhead, no check needed.
/// </summary>
public bool IsPolymorphicCandidate { get; }
/// <summary> /// <summary>
/// Cached [AcStringIntern] attribute value for this property. /// Cached [AcStringIntern] attribute value for this property.
/// null = no attribute (follow global StringInterningMode) /// null = no attribute (follow global StringInterningMode)
@ -66,6 +75,9 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
{ {
IsStringCollectionProperty = IsStringCollection(prop.PropertyType); IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
IsObjectDeclaredType = prop.PropertyType == typeof(object); IsObjectDeclaredType = prop.PropertyType == typeof(object);
IsPolymorphicCandidate = !prop.PropertyType.IsSealed
&& !prop.PropertyType.IsValueType
&& prop.PropertyType != typeof(string);
// All typed getters are initialized in PropertyAccessorBase // All typed getters are initialized in PropertyAccessorBase
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty)) if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))

View File

@ -4,150 +4,151 @@ namespace AyCode.Core.Serializers.Binaries;
/// <summary> /// <summary>
/// Binary type codes for serialization. /// Binary type codes for serialization.
/// Designed for fast switch dispatch and compact storage. /// Markers 0..(SlotCount-1) are reserved for object type slot indices (FixObj).
/// Lower 5 bits = type code (0-31) /// All other type markers are defined relative to SlotCount.
/// Upper 3 bits = flags (interned, reference, has-type-info)
/// </summary> /// </summary>
internal static class BinaryTypeCode internal static class BinaryTypeCode
{ {
// Primitive types (0-15) /// <summary>
public const byte Null = 0; /// Number of reserved FixObj slot markers (0..SlotCount-1).
public const byte True = 1; /// When a marker byte is less than SlotCount, it represents an object
public const byte False = 2; /// whose type wrapper is cached at _wrapperSlots[marker].
public const byte Int8 = 3; /// All type markers are defined relative to this constant.
public const byte UInt8 = 4; /// </summary>
public const byte Int16 = 5; public const int SlotCount = 64;
public const byte UInt16 = 6;
public const byte Int32 = 7;
public const byte UInt32 = 8;
public const byte Int64 = 9;
public const byte UInt64 = 10;
public const byte Float32 = 11;
public const byte Float64 = 12;
public const byte Decimal = 13;
public const byte Char = 14;
// String types (16-19)
public const byte String = 16; // Inline UTF8 string (non-interned)
public const byte StringInterned = 17; // Reference to interned string by index (2+ occurrence)
public const byte StringEmpty = 18; // Empty string marker
public const byte StringInternFirst = 19; // First occurrence of interned string - read content + register in cache
// Date/Time types (20-23)
public const byte DateTime = 20;
public const byte DateTimeOffset = 21;
public const byte TimeSpan = 22;
public const byte Guid = 23;
// Enum (24)
public const byte Enum = 24;
// Complex types (25-31)
public const byte Object = 25; // Start of object (non-tracked OR first occurrence when ref tracking)
//public const byte ObjectEnd = 26; // UNUSED — property count is known at compile-time (SGen) or reflection-time (runtime), no end marker needed
public const byte ObjectRef = 27; // Reference to previously serialized object (2+ occurrence)
public const byte Array = 28; // Start of array/list
public const byte Dictionary = 29; // Start of dictionary
public const byte ByteArray = 30; // Optimized byte[] storage
public const byte ObjectWithMetadata = 31; // Object with metadata (UseMetadata mode, non-tracked OR first occurrence)
// Extended markers for first occurrence tracking (66-67, after FixStr range) // Complex types (SlotCount + 0..7)
public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled) public const byte Object = SlotCount + 0; // 64 — Start of object (fallback when >SlotCount types)
public const byte ObjectWithMetadataRefFirst = 67; // First occurrence of tracked object with metadata public const byte ObjectRef = SlotCount + 1; // 65 — Reference to previously serialized object (2+ occurrence)
public const byte Array = SlotCount + 2; // 66 — Start of array/list
// Polymorphic object markers (68-69): self-describing object for polymorphic properties. public const byte Dictionary = SlotCount + 3; // 67 — Start of dictionary
// Used when declared property type ≠ runtime type AND UseMetadata=false. public const byte ByteArray = SlotCount + 4; // 68 — Optimized byte[] storage
// Serializer writes runtime type name inline so deserializer can resolve the concrete type. public const byte ObjectWithMetadata = SlotCount + 5; // 69 — Object with metadata (UseMetadata mode)
// Format: [ObjectWithTypeName (68)] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd] public const byte ObjectRefFirst = SlotCount + 6; // 70 — First occurrence of tracked object (ref handling enabled)
// Format: [ObjectWithTypeNameRefFirst (69)] [VarUInt cacheIndex] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd] public const byte ObjectWithMetadataRefFirst = SlotCount + 7; // 71 — First occurrence of tracked object with metadata
public const byte ObjectWithTypeName = 68;
public const byte ObjectWithTypeNameRefFirst = 69; // Polymorphic object markers (SlotCount + 8..11)
// Used when declared property type != runtime type AND UseMetadata=false.
// Special markers (32+, for header/meta) //
// Header flags byte structure (for values >= 64): // PREFIX markers (inner Object/Array/Dict marker follows):
// Bit 0 (0x01): HasMetadata // [ObjectWithTypeName] [UTF8 typeName] [Object | Array | ...] [body...]
// Bit 1 (0x02): HasReferenceHandling // [ObjectWithTypeIndex] [VarUInt typeIndex] [Object | Array | ...] [body...]
// Values 32, 33 are legacy for backward compatibility //
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true) // COMBINED markers (no inner marker — object body follows directly):
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true) // [ObjectWithTypeNameRefFirst] [UTF8 typeName] [VarUInt refCacheIndex] [properties...]
// [ObjectWithTypeIndexRefFirst] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
// FixStr range: 34-65 (32 values for strings 0-31 bytes) //
// ObjectRef for 2+ occurrence: written directly, NO poly prefix needed.
public const byte ObjectWithTypeName = SlotCount + 8; // 72
public const byte ObjectWithTypeNameRefFirst = SlotCount + 9; // 73
public const byte ObjectWithTypeIndex = SlotCount + 10; // 74
public const byte ObjectWithTypeIndexRefFirst = SlotCount + 11; // 75
// Primitive types (SlotCount + 12..26)
public const byte Null = SlotCount + 12; // 76
public const byte True = SlotCount + 13; // 77
public const byte False = SlotCount + 14; // 78
public const byte Int8 = SlotCount + 15; // 79
public const byte UInt8 = SlotCount + 16; // 80
public const byte Int16 = SlotCount + 17; // 81
public const byte UInt16 = SlotCount + 18; // 82
public const byte Int32 = SlotCount + 19; // 83
public const byte UInt32 = SlotCount + 20; // 84
public const byte Int64 = SlotCount + 21; // 85
public const byte UInt64 = SlotCount + 22; // 86
public const byte Float32 = SlotCount + 23; // 87
public const byte Float64 = SlotCount + 24; // 88
public const byte Decimal = SlotCount + 25; // 89
public const byte Char = SlotCount + 26; // 90
// String types (SlotCount + 27..30)
public const byte String = SlotCount + 27; // 91 — Inline UTF8 string (non-interned)
public const byte StringInterned = SlotCount + 28; // 92 — Reference to interned string by index (2+ occurrence)
public const byte StringEmpty = SlotCount + 29; // 93 — Empty string marker
public const byte StringInternFirst = SlotCount + 30; // 94 — First occurrence of interned string
// Date/Time types (SlotCount + 31..34)
public const byte DateTime = SlotCount + 31; // 95
public const byte DateTimeOffset = SlotCount + 32; // 96
public const byte TimeSpan = SlotCount + 33; // 97
public const byte Guid = SlotCount + 34; // 98
// Enum (SlotCount + 35)
public const byte Enum = SlotCount + 35; // 99
// Legacy header markers (SlotCount + 36..37)
public const byte MetadataHeader = SlotCount + 36; // 100 — Binary has metadata section (legacy, implies HasReferenceHandling=true)
public const byte NoMetadataHeader = SlotCount + 37; // 101 — Binary has no metadata (legacy, implies HasReferenceHandling=true)
// Property skip marker (SlotCount + 38)
public const byte PropertySkip = SlotCount + 38; // 102 — Marks a property with default/null value (skipped during serialization)
// FixStr range: SlotCount + 39 .. SlotCount + 70 (32 values for strings 0-31 bytes)
// FixStr encoding: FixStrBase + length (0-31) // FixStr encoding: FixStrBase + length (0-31)
// This saves 1 byte for short strings by combining type + length in single byte // This saves 1 byte for short strings by combining type + length in single byte
public const byte FixStrBase = 34; // Base value for FixStr (0xA0 in MessagePack style, but we use 34) public const byte FixStrBase = SlotCount + 39; // 103
public const byte FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code public const byte FixStrMax = FixStrBase + 31; // 134
public const int FixStrMaxLength = 31; // Maximum string length encodable as FixStr public const int FixStrMaxLength = 31;
// New flag-based header markers (48+) - moved to after FixStr range // Flag-based header markers (must be 16-aligned for flag bits in lower nibble)
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
// Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F) // Header byte structure: (marker & 0xF0) == HeaderFlagsBase, flags in (marker & 0x0F)
public const byte HeaderFlagsBase = 48; // Base value for flag-based headers (0x30) public const byte HeaderFlagsBase = 144; // 0x90 — next 16-aligned value after FixStrMax
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
// Reference handling uses 2 separate bits: // Reference handling uses 2 separate bits:
// Bit 1 (0x02): OnlyId - reference handling for IId objects only // Bit 1 (0x02): OnlyId - reference handling for IId objects only
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId) // Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set) // None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
public const byte HeaderFlag_RefHandling_OnlyId = 0x02; public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
public const byte HeaderFlag_RefHandling_All = 0x04; public const byte HeaderFlag_RefHandling_All = 0x04;
public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags (legacy) public const byte HeaderFlag_HasFooterPosition = 0x08; // Bit 3: 4-byte footer position follows flags (legacy)
public const byte HeaderFlag_HasCacheCount = 0x08; // Bit 3 (reused): VarUInt cache count follows flags (new marker-based format) public const byte HeaderFlag_HasCacheCount = 0x08; // Bit 3 (reused): VarUInt cache count follows flags (new marker-based format)
// Compact integer variants (for VarInt optimization) // Compact integer variants (unchanged)
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16) public const byte Int32Tiny = 192; // -16 to 47 stored in single byte (value = code - 192 - 16)
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255) public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
// Property skip marker (for single-pass serialization optimization)
// CRITICAL: Must be in the "reserved" range 67-191 (after FixStr, before TinyInt)
// AND must not conflict with any other type codes.
// Using 191 (0xBF) - the highest value before TinyInt range starts at 192.
// This ensures it won't be confused with:
// - Primitive types (0-31)
// - FixStr (34-65)
// - TinyInt values (192-255)
public const byte PropertySkip = 191; // Marks a property with default/null value (skipped during serialization)
/// <summary> /// <summary>
/// Check if type code represents a reference (string or object). /// Check if type code represents a reference (string or object).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsReference(byte code) => code is StringInterned or ObjectRef; public static bool IsReference(byte code) => code is StringInterned or ObjectRef;
/// <summary> /// <summary>
/// Check if type code is a FixStr (short string with length encoded in type code). /// Check if type code is a FixStr (short string with length encoded in type code).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax; public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax;
/// <summary> /// <summary>
/// Decode FixStr length from type code. /// Decode FixStr length from type code.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int DecodeFixStrLength(byte code) => code - FixStrBase; public static int DecodeFixStrLength(byte code) => code - FixStrBase;
/// <summary> /// <summary>
/// Encode FixStr type code for given byte length (0-31). /// Encode FixStr type code for given byte length (0-31).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength); public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength);
/// <summary> /// <summary>
/// Check if byte length can be encoded as FixStr. /// Check if byte length can be encoded as FixStr.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31; public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31;
/// <summary> /// <summary>
/// Check if type code is a tiny int (single byte int32 encoding). /// Check if type code is a tiny int (single byte int32 encoding).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsTinyInt(byte code) => code >= Int32Tiny; public static bool IsTinyInt(byte code) => code >= Int32Tiny;
/// <summary> /// <summary>
/// Decode tiny int value from type code. /// Decode tiny int value from type code.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16; public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
/// <summary> /// <summary>
/// Encode small int value (-16 to 47) as type code. /// Encode small int value (-16 to 47) as type code.
/// Returns true if value fits in tiny encoding. /// Returns true if value fits in tiny encoding.
@ -164,4 +165,4 @@ internal static class BinaryTypeCode
code = 0; code = 0;
return false; return false;
} }
} }

View File

@ -43,6 +43,21 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary> /// </summary>
internal bool MetadataSeen; internal bool MetadataSeen;
/// <summary>
/// Polymorphic type tracking: has this runtime type been written as a polymorphic prefix?
/// false = first occurrence → write ObjectWithTypeName (68) + full type name.
/// true = repeated → write ObjectWithTypeIndex (70) + PolymorphicCacheIndex.
/// Same pattern as MetadataSeen. Reset by ResetTracking.
/// </summary>
internal bool PolymorphicSeen;
/// <summary>
/// Unified type slot index for FixObj system. Used by both poly and non-poly types.
/// -1 = not yet assigned. Set together with PolymorphicSeen = true.
/// Reset by ResetTracking (per-session, slot order depends on stream encounter order).
/// </summary>
internal int PolymorphicCacheIndex = -1;
/// <summary> /// <summary>
/// UseMetadata cachemap: source property index → target PropertySetter. /// UseMetadata cachemap: source property index → target PropertySetter.
/// Per-context (wrapper-szintű), mert futásonként eltérő source type-pal találkozhat. /// Per-context (wrapper-szintű), mert futásonként eltérő source type-pal találkozhat.
@ -193,6 +208,8 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
public void ResetTracking(bool preRentBuckets = false) public void ResetTracking(bool preRentBuckets = false)
{ {
MetadataSeen = false; MetadataSeen = false;
PolymorphicSeen = false;
PolymorphicCacheIndex = -1;
CacheMap = null; CacheMap = null;
// Options may change between sessions (pool reuse) → rebuild on next scan // Options may change between sessions (pool reuse) → rebuild on next scan