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:
parent
11a15bfa64
commit
68c25b2381
|
|
@ -44,7 +44,9 @@
|
|||
"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.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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,14 @@ public static class Program
|
|||
// Serializer name constants
|
||||
private const string SerializerMessagePack = "MessagePack";
|
||||
private const string SerializerAcBinaryDefault = "AcBinary (Default)";
|
||||
private const string SerializerAcBinaryDefaultNoSgen = "AcBinary (Def, NoSgen)";
|
||||
private const string SerializerAcBinaryNoRef = "AcBinary (NoRef)";
|
||||
private const string SerializerAcBinaryFastMode = "AcBinary (FastMode)";
|
||||
private const string SerializerAcBinaryFastNoSgen = "AcBinary (Fast, NoSgen)";
|
||||
private const string SerializerAcBinaryNoIntern = "AcBinary (NoIntern)";
|
||||
private const string SerializerMemoryPack = "MemoryPack";
|
||||
private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)";
|
||||
private const string SerializerSystemTextJson = "System.Text.Json";
|
||||
//private const string SerializerAcBinaryBufferWriter = "AcBinary (BufferWriter)";
|
||||
//private const string SerializerSystemTextJson = "System.Text.Json";
|
||||
|
||||
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
|
|
@ -208,20 +210,34 @@ public static class Program
|
|||
|
||||
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>
|
||||
{
|
||||
|
||||
// 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, 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, 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),
|
||||
|
||||
|
||||
// MemoryPack
|
||||
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
||||
|
||||
|
|
@ -229,10 +245,10 @@ public static class Program
|
|||
new MessagePackBenchmark(testData.Order, SerializerMessagePack),
|
||||
|
||||
// AcBinary BufferWriter
|
||||
new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
|
||||
//new AcBinaryBufferWriterBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryBufferWriter),
|
||||
|
||||
// System.Text.Json
|
||||
new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
||||
//new SystemTextJsonBenchmark(testData.Order, SerializerSystemTextJson)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -583,7 +583,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
sb.AppendLine();
|
||||
sb.AppendLine(" if (context.HasRefHandling)");
|
||||
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($" if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
|
||||
sb.AppendLine(" {");
|
||||
|
|
@ -600,7 +600,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
sb.AppendLine();
|
||||
sb.AppendLine(" if (context.HasAllRefHandling)");
|
||||
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(" if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
|
||||
sb.AppendLine(" {");
|
||||
|
|
@ -1227,7 +1227,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
else
|
||||
{
|
||||
// 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} {{");
|
||||
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
||||
|
|
@ -1267,7 +1267,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
else
|
||||
{
|
||||
// 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
|
||||
? "context.HasRefHandling"
|
||||
: "context.HasAllRefHandling";
|
||||
|
|
@ -1398,7 +1398,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
else
|
||||
{
|
||||
// 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} {{");
|
||||
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
||||
|
|
@ -1442,7 +1442,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
else
|
||||
{
|
||||
// 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} {{");
|
||||
sb.AppendLine($"{i} if (!epe_{p.Name}.IsFirst)");
|
||||
|
|
@ -1655,7 +1655,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
else
|
||||
{
|
||||
// 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} {{");
|
||||
sb.AppendLine($"{i} context.WriteByte(BinaryTypeCode.ObjectWithMetadata);");
|
||||
|
|
@ -1698,7 +1698,7 @@ public class AcBinarySourceGenerator : IIncrementalGenerator
|
|||
else
|
||||
{
|
||||
// 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} {{");
|
||||
sb.AppendLine($"{i} if (!dpe_{s}.IsFirst)");
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public class AcBinarySerializerBasicTests
|
|||
{
|
||||
var result = AcBinarySerializer.Serialize<object?>(null);
|
||||
Assert.AreEqual(1, result.Length);
|
||||
Assert.AreEqual((byte)0, result[0]);
|
||||
Assert.AreEqual(BinaryTypeCode.Null, result[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
|
|||
|
|
@ -55,8 +55,9 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
|||
private readonly Dictionary<Type, TypeMetadataWrapper<TMetadata>> _wrappers = new();
|
||||
|
||||
/// <summary>
|
||||
/// Slot-indexed wrapper cache for SGen types. Indexed by AllocateWrapperSlot() value.
|
||||
/// Avoids dictionary lookup — direct array access for types with compile-time known slot index.
|
||||
/// Slot-indexed wrapper cache. Shared between SGen types (RuntimeSlotCount+) and
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private TypeMetadataWrapper<TMetadata>?[]? _wrapperSlots;
|
||||
|
|
@ -107,33 +108,35 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates a wrapper for the specified type using a pre-allocated slot index.
|
||||
/// SGen types call this with their compile-time known slot — avoids dictionary lookup.
|
||||
/// First call per slot per context: falls back to GetWrapper + stores in slot array.
|
||||
/// Subsequent calls: direct array index (~1-2ns vs ~15-25ns dictionary lookup).
|
||||
/// Gets or creates a wrapper for the specified type using a slot index.
|
||||
/// Slot checked first (array access ~1-2ns), falls back to dictionary if slot empty.
|
||||
/// Used by both SGen (compile-time slot) and runtime polymorphic cache (0..RuntimeSlotCount-1).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TypeMetadataWrapper<TMetadata> GetWrapperBySlot(int slot, Type type)
|
||||
public TypeMetadataWrapper<TMetadata> GetWrapper(Type type, int slotIndex)
|
||||
{
|
||||
var slots = _wrapperSlots;
|
||||
if (slots != null && slot < slots.Length)
|
||||
{
|
||||
var wrapper = slots[slot];
|
||||
if (wrapper != null)
|
||||
return wrapper;
|
||||
}
|
||||
var slots = _wrapperSlots!;
|
||||
var wrapper = slots[slotIndex];
|
||||
if (wrapper != null)
|
||||
return wrapper;
|
||||
|
||||
return GetWrapperBySlotSlow(slot, type);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private TypeMetadataWrapper<TMetadata> GetWrapperBySlotSlow(int slot, Type type)
|
||||
{
|
||||
var wrapper = GetWrapper(type);
|
||||
_wrapperSlots![slot] = wrapper;
|
||||
wrapper = GetWrapper(type);
|
||||
slots[slotIndex] = 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>
|
||||
/// Pre-allocates the wrapper slot array with the known total slot count.
|
||||
/// Called once from derived context constructor after all AllocateWrapperSlot() calls have completed.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Buffers;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
|
|
@ -104,6 +105,19 @@ public static partial class AcBinaryDeserializer
|
|||
private MetadataEntry[]? _metadataEntries;
|
||||
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>
|
||||
/// A metadata entry for the deserializer.
|
||||
/// </summary>
|
||||
|
|
@ -121,6 +135,7 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
public BinaryDeserializationContext()
|
||||
{
|
||||
InitializeWrapperSlots(Volatile.Read(ref AcBinarySerializer.s_nextWrapperSlot));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -367,6 +382,54 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
#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
|
||||
|
||||
public override void Reset(AcBinarySerializerOptions options)
|
||||
|
|
@ -381,6 +444,17 @@ public static partial class AcBinaryDeserializer
|
|||
_metadataEntryCount = 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
|
||||
_stringCache?.Clear();
|
||||
|
||||
|
|
|
|||
|
|
@ -102,17 +102,26 @@ public static partial class AcBinaryDeserializer
|
|||
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
|
||||
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
|
||||
readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
|
||||
readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst;
|
||||
readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex;
|
||||
readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst;
|
||||
readers[BinaryTypeCode.Array] = ReadArray;
|
||||
readers[BinaryTypeCode.Dictionary] = ReadDictionary;
|
||||
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
|
||||
|
||||
// Register FixStr readers (34-65)
|
||||
// Register FixStr readers
|
||||
for (byte code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
|
||||
{
|
||||
var length = BinaryTypeCode.DecodeFixStrLength(code);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -131,6 +140,16 @@ public static partial class AcBinaryDeserializer
|
|||
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);
|
||||
|
||||
#region Public API
|
||||
|
|
@ -908,16 +927,16 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
var typeCode = context.ReadByte();
|
||||
|
||||
// Handle null first
|
||||
if (typeCode == BinaryTypeCode.Null) return null;
|
||||
|
||||
// Handle tiny int (most common case for small integers)
|
||||
// Handle tiny int first (most common case for small integers, >= 192)
|
||||
if (BinaryTypeCode.IsTinyInt(typeCode))
|
||||
{
|
||||
var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
|
||||
return ConvertToTargetType(intValue, targetType);
|
||||
}
|
||||
|
||||
// Handle null
|
||||
if (typeCode == BinaryTypeCode.Null) return null;
|
||||
|
||||
// Handle FixStr (short strings with length in type code)
|
||||
if (BinaryTypeCode.IsFixStr(typeCode))
|
||||
{
|
||||
|
|
@ -1124,6 +1143,36 @@ public static partial class AcBinaryDeserializer
|
|||
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>
|
||||
/// Object olvasása (nem tracked, vagy UseMetadata nélkül).
|
||||
/// Wire format: [Object][props...]
|
||||
|
|
@ -1147,10 +1196,10 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polymorphic object prefix: declared property type is System.Object.
|
||||
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [Object (25) or ObjectRefFirst (66) ...] [props...]
|
||||
/// Reads the runtime type name, resolves it, then delegates to ReadValue with the resolved type
|
||||
/// so the next marker (Object/ObjectRefFirst/etc.) is processed normally.
|
||||
/// Polymorphic PREFIX marker: declared type ≠ runtime type.
|
||||
/// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...]
|
||||
/// Reads the runtime type name, resolves it, registers wrapper in poly slot cache,
|
||||
/// then reads the inner marker via ReadValue.
|
||||
/// </summary>
|
||||
private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int depth)
|
||||
where TInput : struct, IBinaryInputBase
|
||||
|
|
@ -1160,10 +1209,59 @@ public static partial class AcBinaryDeserializer
|
|||
?? throw new AcBinaryDeserializationException(
|
||||
$"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Object olvasás core implementáció.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -160,6 +160,12 @@ public static partial class AcBinarySerializer
|
|||
/// </summary>
|
||||
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>
|
||||
/// Tries to consume the next write plan entry at the current WriteVisitIndex.
|
||||
/// Returns true if the entry matches (duplicate exists at this visit point).
|
||||
|
|
@ -301,6 +307,7 @@ public static partial class AcBinarySerializer
|
|||
WriteVisitIndex = 0;
|
||||
_nextWritePlanVisitIndex = int.MaxValue;
|
||||
StringInternEligible = false;
|
||||
_nextTypeSlot = 0;
|
||||
|
||||
// Clear write plan string references to avoid GC pinning, keep array if small enough
|
||||
if (_writePlan != null)
|
||||
|
|
@ -906,6 +913,61 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#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
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -248,12 +248,22 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#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>
|
||||
/// Allocates a unique slot index for SGen wrapper cache.
|
||||
/// Indexes _wrapperSlots array on AcSerializerContextBase.
|
||||
/// Used for: IdentityMap ref tracking (scan pass), MetadataSeen (write pass).
|
||||
/// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ...
|
||||
/// </summary>
|
||||
internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;
|
||||
|
||||
|
|
@ -574,7 +584,7 @@ public static partial class AcBinarySerializer
|
|||
return;
|
||||
}
|
||||
|
||||
var wrapper = context.GetWrapperBySlot(wrapperSlot, type);
|
||||
var wrapper = context.GetWrapper(type, wrapperSlot);
|
||||
WriteObject(value, wrapper, context, depth);
|
||||
}
|
||||
|
||||
|
|
@ -651,8 +661,9 @@ public static partial class AcBinarySerializer
|
|||
/// <summary>
|
||||
/// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
|
||||
/// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
|
||||
/// When polyRuntimeType is set, writes polymorphic prefix/combined markers.
|
||||
/// </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
|
||||
{
|
||||
var type = wrapper.Metadata.SourceType;
|
||||
|
|
@ -666,6 +677,7 @@ public static partial class AcBinarySerializer
|
|||
|
||||
if (depth > context.MaxDepth)
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
return;
|
||||
}
|
||||
|
|
@ -673,6 +685,7 @@ public static partial class AcBinarySerializer
|
|||
// Handle byte arrays specially (value-like, no reference tracking)
|
||||
if (value is byte[] byteArray)
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteByteArray(byteArray, context);
|
||||
return;
|
||||
}
|
||||
|
|
@ -680,6 +693,7 @@ public static partial class AcBinarySerializer
|
|||
// Handle dictionaries
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteDictionary(dictionary, context, depth);
|
||||
return;
|
||||
}
|
||||
|
|
@ -687,12 +701,13 @@ public static partial class AcBinarySerializer
|
|||
// Handle collections/arrays
|
||||
if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
|
||||
{
|
||||
if (polyRuntimeType != null) context.WritePolymorphicPrefix(polyRuntimeType);
|
||||
WriteArray(enumerable, wrapper, context, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle complex objects with single-pass reference tracking
|
||||
WriteObject(value, wrapper, context, depth);
|
||||
// Handle complex objects — combined poly+ref markers handled inside WriteObject
|
||||
WriteObject(value, wrapper, context, depth, polyRuntimeType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1058,7 +1073,7 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#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
|
||||
{
|
||||
var metadata = wrapper.Metadata;
|
||||
|
|
@ -1089,7 +1104,8 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
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.WriteVarUInt((uint)planEntry.CacheMapIndex);
|
||||
return;
|
||||
|
|
@ -1097,10 +1113,12 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
// Marker kiírása:
|
||||
// - Cached object first occurrence: ObjectRefFirst/ObjectWithMetadataRefFirst + cacheIndex
|
||||
// - Non-cached: Object/ObjectWithMetadata
|
||||
if (useMetaForType)
|
||||
// Marker kiírása — polymorphic vs non-polymorphic paths
|
||||
if (polyRuntimeType != null)
|
||||
{
|
||||
WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);
|
||||
}
|
||||
else if (useMetaForType)
|
||||
{
|
||||
if (cachedObjectCacheIndex >= 0)
|
||||
{
|
||||
|
|
@ -1122,7 +1140,16 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
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>
|
||||
/// Checks if a property value is null or default without boxing for value types.
|
||||
/// </summary>
|
||||
|
|
@ -1500,14 +1560,11 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
var runtimeType = value.GetType();
|
||||
|
||||
// System.Object declared property → prefix with ObjectWithTypeName marker + TypeName
|
||||
// so the deserializer can resolve the concrete runtime type.
|
||||
// The normal Object/ObjectRefFirst marker follows as usual.
|
||||
if (prop.IsObjectDeclaredType && !context.UseMetadata)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectWithTypeName);
|
||||
context.WriteStringUtf8(runtimeType.AssemblyQualifiedName!);
|
||||
}
|
||||
// Polymorphic detection: when declared type ≠ runtime type, pass polyRuntimeType
|
||||
// to WriteValueNonPrimitiveWithWrapper → WriteObject for combined marker handling.
|
||||
// For collections: normal prefix pattern (68/70 + inner Array/Dict marker).
|
||||
// For objects: combined markers (69/71) when RefFirst, no prefix for ObjectRef.
|
||||
var isPoly = !context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType;
|
||||
|
||||
var complexIdx = prop.ComplexPropertyIndex;
|
||||
if (complexIdx >= 0)
|
||||
|
|
@ -1518,11 +1575,12 @@ public static partial class AcBinarySerializer
|
|||
propWrapper = context.GetWrapper(runtimeType);
|
||||
parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
|
||||
}
|
||||
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth);
|
||||
WriteValueNonPrimitiveWithWrapper(value, propWrapper, context, depth, isPoly ? runtimeType : null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-complex in default case (nullable value type, etc.)
|
||||
if (isPoly) context.WritePolymorphicPrefix(runtimeType);
|
||||
WriteValueNonPrimitive(value, runtimeType, context, depth);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
/// </summary>
|
||||
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>
|
||||
/// Cached [AcStringIntern] attribute value for this property.
|
||||
/// null = no attribute (follow global StringInterningMode)
|
||||
|
|
@ -66,6 +75,9 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
{
|
||||
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
|
||||
IsObjectDeclaredType = prop.PropertyType == typeof(object);
|
||||
IsPolymorphicCandidate = !prop.PropertyType.IsSealed
|
||||
&& !prop.PropertyType.IsValueType
|
||||
&& prop.PropertyType != typeof(string);
|
||||
|
||||
// All typed getters are initialized in PropertyAccessorBase
|
||||
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))
|
||||
|
|
|
|||
|
|
@ -4,150 +4,151 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
|
||||
/// <summary>
|
||||
/// Binary type codes for serialization.
|
||||
/// Designed for fast switch dispatch and compact storage.
|
||||
/// Lower 5 bits = type code (0-31)
|
||||
/// Upper 3 bits = flags (interned, reference, has-type-info)
|
||||
/// Markers 0..(SlotCount-1) are reserved for object type slot indices (FixObj).
|
||||
/// All other type markers are defined relative to SlotCount.
|
||||
/// </summary>
|
||||
internal static class BinaryTypeCode
|
||||
{
|
||||
// Primitive types (0-15)
|
||||
public const byte Null = 0;
|
||||
public const byte True = 1;
|
||||
public const byte False = 2;
|
||||
public const byte Int8 = 3;
|
||||
public const byte UInt8 = 4;
|
||||
public const byte Int16 = 5;
|
||||
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)
|
||||
/// <summary>
|
||||
/// Number of reserved FixObj slot markers (0..SlotCount-1).
|
||||
/// When a marker byte is less than SlotCount, it represents an object
|
||||
/// whose type wrapper is cached at _wrapperSlots[marker].
|
||||
/// All type markers are defined relative to this constant.
|
||||
/// </summary>
|
||||
public const int SlotCount = 64;
|
||||
|
||||
// Extended markers for first occurrence tracking (66-67, after FixStr range)
|
||||
public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled)
|
||||
public const byte ObjectWithMetadataRefFirst = 67; // First occurrence of tracked object with metadata
|
||||
|
||||
// Polymorphic object markers (68-69): self-describing object for polymorphic properties.
|
||||
// Used when declared property type ≠ runtime type AND UseMetadata=false.
|
||||
// Serializer writes runtime type name inline so deserializer can resolve the concrete type.
|
||||
// Format: [ObjectWithTypeName (68)] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd]
|
||||
// Format: [ObjectWithTypeNameRefFirst (69)] [VarUInt cacheIndex] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd]
|
||||
public const byte ObjectWithTypeName = 68;
|
||||
public const byte ObjectWithTypeNameRefFirst = 69;
|
||||
|
||||
// Special markers (32+, for header/meta)
|
||||
// Header flags byte structure (for values >= 64):
|
||||
// Bit 0 (0x01): HasMetadata
|
||||
// Bit 1 (0x02): HasReferenceHandling
|
||||
// Values 32, 33 are legacy for backward compatibility
|
||||
public const byte MetadataHeader = 32; // Binary has metadata section (legacy, implies HasReferenceHandling=true)
|
||||
public const byte NoMetadataHeader = 33; // Binary has no metadata (legacy, implies HasReferenceHandling=true)
|
||||
|
||||
// FixStr range: 34-65 (32 values for strings 0-31 bytes)
|
||||
// Complex types (SlotCount + 0..7)
|
||||
public const byte Object = SlotCount + 0; // 64 — Start of object (fallback when >SlotCount types)
|
||||
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
|
||||
public const byte Dictionary = SlotCount + 3; // 67 — Start of dictionary
|
||||
public const byte ByteArray = SlotCount + 4; // 68 — Optimized byte[] storage
|
||||
public const byte ObjectWithMetadata = SlotCount + 5; // 69 — Object with metadata (UseMetadata mode)
|
||||
public const byte ObjectRefFirst = SlotCount + 6; // 70 — First occurrence of tracked object (ref handling enabled)
|
||||
public const byte ObjectWithMetadataRefFirst = SlotCount + 7; // 71 — First occurrence of tracked object with metadata
|
||||
|
||||
// Polymorphic object markers (SlotCount + 8..11)
|
||||
// Used when declared property type != runtime type AND UseMetadata=false.
|
||||
//
|
||||
// PREFIX markers (inner Object/Array/Dict marker follows):
|
||||
// [ObjectWithTypeName] [UTF8 typeName] [Object | Array | ...] [body...]
|
||||
// [ObjectWithTypeIndex] [VarUInt typeIndex] [Object | Array | ...] [body...]
|
||||
//
|
||||
// COMBINED markers (no inner marker — object body follows directly):
|
||||
// [ObjectWithTypeNameRefFirst] [UTF8 typeName] [VarUInt refCacheIndex] [properties...]
|
||||
// [ObjectWithTypeIndexRefFirst] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
|
||||
//
|
||||
// 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)
|
||||
// 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 FixStrMax = 65; // FixStrBase + 31 = maximum FixStr code
|
||||
public const int FixStrMaxLength = 31; // Maximum string length encodable as FixStr
|
||||
|
||||
// New flag-based header markers (48+) - moved to after FixStr range
|
||||
// Note: FixStr range 34-65 overlaps with old HeaderFlagsBase, so headers use version byte prefix
|
||||
public const byte FixStrBase = SlotCount + 39; // 103
|
||||
public const byte FixStrMax = FixStrBase + 31; // 134
|
||||
public const int FixStrMaxLength = 31;
|
||||
|
||||
// Flag-based header markers (must be 16-aligned for flag bits in lower nibble)
|
||||
// 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 HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
|
||||
public const byte HeaderFlagsBase = 144; // 0x90 — next 16-aligned value after FixStrMax
|
||||
public const byte HeaderFlag_Metadata = 0x01; // Bit 0: property metadata included
|
||||
// Reference handling uses 2 separate bits:
|
||||
// Bit 1 (0x02): OnlyId - reference handling for IId objects only
|
||||
// Bit 2 (0x04): All - reference handling for all objects (includes OnlyId)
|
||||
// None = both false, OnlyId = 0x02, All = 0x06 (both bits set)
|
||||
public const byte HeaderFlag_RefHandling_OnlyId = 0x02;
|
||||
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_HasCacheCount = 0x08; // Bit 3 (reused): VarUInt cache count follows flags (new marker-based format)
|
||||
|
||||
// Compact integer variants (for VarInt optimization)
|
||||
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
|
||||
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)
|
||||
|
||||
// Compact integer variants (unchanged)
|
||||
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)
|
||||
|
||||
// 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>
|
||||
/// Check if type code represents a reference (string or object).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsReference(byte code) => code is StringInterned or ObjectRef;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a FixStr (short string with length encoded in type code).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsFixStr(byte code) => code is >= FixStrBase and <= FixStrMax;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decode FixStr length from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeFixStrLength(byte code) => code - FixStrBase;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Encode FixStr type code for given byte length (0-31).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte EncodeFixStr(int byteLength) => (byte)(FixStrBase + byteLength);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if byte length can be encoded as FixStr.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CanEncodeAsFixStr(int byteLength) => byteLength is >= 0 and <= 31;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code is a tiny int (single byte int32 encoding).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsTinyInt(byte code) => code >= Int32Tiny;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decode tiny int value from type code.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int DecodeTinyInt(byte code) => code - Int32Tiny - 16;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Encode small int value (-16 to 47) as type code.
|
||||
/// Returns true if value fits in tiny encoding.
|
||||
|
|
@ -164,4 +165,4 @@ internal static class BinaryTypeCode
|
|||
code = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,21 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
/// </summary>
|
||||
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>
|
||||
/// UseMetadata cachemap: source property index → target PropertySetter.
|
||||
/// 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)
|
||||
{
|
||||
MetadataSeen = false;
|
||||
PolymorphicSeen = false;
|
||||
PolymorphicCacheIndex = -1;
|
||||
CacheMap = null;
|
||||
|
||||
// Options may change between sessions (pool reuse) → rebuild on next scan
|
||||
|
|
|
|||
Loading…
Reference in New Issue