Generate IGeneratedBinaryWriter for fast serialization
Refactor source generator to emit per-type IGeneratedBinaryWriter classes for [AcBinarySerializable] types, with auto-registration at startup. Integrate generated writers into AcBinarySerializer for direct, delegate-free property writing, bypassing the runtime property loop when possible. Add registry, bridge methods, and update TypeMetadataWrapper for fast lookup. Expand tests to verify generated writers and round-trip correctness. This enables major serialization performance gains and reduces code size for supported types.
This commit is contained in:
parent
896f720109
commit
4ef65ee501
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,141 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Tests.TestModels;
|
||||
|
||||
namespace AyCode.Core.Tests.GeneratedWriters;
|
||||
|
||||
/// <summary>
|
||||
/// Hand-written generated binary writer for TestOrder.
|
||||
/// Demonstrates the pattern that the source generator will produce.
|
||||
///
|
||||
/// Bypasses the runtime switch/delegate property loop:
|
||||
/// - Direct obj.Property access instead of Func<>.Invoke()
|
||||
/// - No switch dispatch per property
|
||||
/// - No boxing for value types
|
||||
/// - Small method (~500B native) vs 27KB WriteObject — better ICache
|
||||
///
|
||||
/// Properties are written in alphabetical order to match the runtime serializer.
|
||||
/// Complex/Collection properties fall back to the runtime serializer via WriteValue.
|
||||
/// </summary>
|
||||
internal sealed class TestOrderWriter : IGeneratedBinaryWriter
|
||||
{
|
||||
internal static readonly TestOrderWriter Instance = new();
|
||||
|
||||
public void WriteProperties<TOutput>(
|
||||
object value,
|
||||
AcBinarySerializer.BinarySerializationContext<TOutput> context,
|
||||
int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
var obj = Unsafe.As<TestOrder>(value);
|
||||
var nextDepth = depth;
|
||||
|
||||
// Properties in alphabetical order (matching runtime serializer):
|
||||
|
||||
// AuditMetadata: MetadataInfo? (complex, nullable)
|
||||
WriteComplexOrNull(obj.AuditMetadata, context, nextDepth);
|
||||
|
||||
// Category: SharedCategory? (complex, nullable)
|
||||
WriteComplexOrNull(obj.Category, context, nextDepth);
|
||||
|
||||
// CreatedAt: DateTime
|
||||
context.WriteByte(BinaryTypeCode.DateTime);
|
||||
context.WriteDateTimeBits(obj.CreatedAt);
|
||||
|
||||
// Id: int
|
||||
WriteInt32OrSkip(obj.Id, context);
|
||||
|
||||
// Items: List<TestOrderItem> (collection)
|
||||
WriteComplexOrNull(obj.Items, context, nextDepth);
|
||||
|
||||
// MetadataList: List<MetadataInfo> (collection)
|
||||
WriteComplexOrNull(obj.MetadataList, context, nextDepth);
|
||||
|
||||
// NoMergeItems: List<TestOrderItem> (collection)
|
||||
WriteComplexOrNull(obj.NoMergeItems, context, nextDepth);
|
||||
|
||||
// OrderMetadata: MetadataInfo? (complex, nullable)
|
||||
WriteComplexOrNull(obj.OrderMetadata, context, nextDepth);
|
||||
|
||||
// OrderNumber: string
|
||||
AcBinarySerializer.WriteStringGenerated(obj.OrderNumber, context);
|
||||
|
||||
// Owner: SharedUser? (complex, nullable)
|
||||
WriteComplexOrNull(obj.Owner, context, nextDepth);
|
||||
|
||||
// PaidDateUtc: DateTime? (nullable)
|
||||
var paidDate = obj.PaidDateUtc;
|
||||
if (paidDate.HasValue)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.DateTime);
|
||||
context.WriteDateTimeBits(paidDate.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
}
|
||||
|
||||
// PrimaryTag: SharedTag? (complex, nullable)
|
||||
WriteComplexOrNull(obj.PrimaryTag, context, nextDepth);
|
||||
|
||||
// SecondaryTag: SharedTag? (complex, nullable)
|
||||
WriteComplexOrNull(obj.SecondaryTag, context, nextDepth);
|
||||
|
||||
// Status: TestStatus (enum)
|
||||
context.WriteByte(BinaryTypeCode.Enum);
|
||||
var enumVal = (int)obj.Status;
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(enumVal, out var tinyEnum))
|
||||
context.WriteByte(tinyEnum);
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Int32);
|
||||
context.WriteVarInt(enumVal);
|
||||
}
|
||||
|
||||
// Tags: List<SharedTag> (collection)
|
||||
WriteComplexOrNull(obj.Tags, context, nextDepth);
|
||||
|
||||
// TotalAmount: decimal
|
||||
if (obj.TotalAmount == 0m)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Decimal);
|
||||
context.WriteDecimalBits(obj.TotalAmount);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteInt32OrSkip<TOutput>(int value, AcBinarySerializer.BinarySerializationContext<TOutput> context)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
if (value == 0)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Int32);
|
||||
context.WriteByte(tiny);
|
||||
return;
|
||||
}
|
||||
|
||||
context.WriteByte(BinaryTypeCode.Int32);
|
||||
context.WriteVarInt(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteComplexOrNull<TOutput>(object? value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
return;
|
||||
}
|
||||
|
||||
AcBinarySerializer.WriteValueGenerated(value, value.GetType(), context, depth);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,98 +4,37 @@ using AyCode.Core.Tests.TestModels;
|
|||
namespace AyCode.Core.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for Source Generator based serialization integration.
|
||||
/// Tests for Source Generator based IGeneratedBinaryWriter integration.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class GeneratedSerializerIntegrationTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void GeneratedSerializerType_Exists_ForMarkedTypes()
|
||||
public void GeneratedWriterType_Exists_ForMarkedTypes()
|
||||
{
|
||||
// Arrange - types marked with [AcBinarySerializable]
|
||||
var type = typeof(GeneratedSerializerTestModel);
|
||||
var writerTypeName = $"{type.FullName}_GeneratedWriter";
|
||||
var writerType = type.Assembly.GetType(writerTypeName);
|
||||
|
||||
// Act - find the generated serializer type directly
|
||||
var generatedTypeName = $"{type.FullName}_AcBinarySerializer";
|
||||
var serializerType = type.Assembly.GetType(generatedTypeName);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(serializerType,
|
||||
$"Generated serializer type '{generatedTypeName}' should exist for [AcBinarySerializable] marked type");
|
||||
Assert.IsNotNull(writerType,
|
||||
$"Generated writer type '{writerTypeName}' should exist for [AcBinarySerializable] marked type");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GeneratedSerializerType_HasCorrectMethods()
|
||||
public void GeneratedWriterType_ImplementsInterface()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(SimpleGeneratedModel);
|
||||
var writerTypeName = $"{type.FullName}_GeneratedWriter";
|
||||
var writerType = type.Assembly.GetType(writerTypeName);
|
||||
|
||||
// Act - find the generated serializer type directly
|
||||
var generatedTypeName = $"{type.FullName}_AcBinarySerializer";
|
||||
var serializerType = type.Assembly.GetType(generatedTypeName);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(serializerType, $"Generated serializer type '{generatedTypeName}' should exist");
|
||||
|
||||
var serializeMethod = serializerType.GetMethod("Serialize",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
Assert.IsNotNull(serializeMethod, "Serialize method should exist");
|
||||
|
||||
var deserializeMethod = serializerType.GetMethod("Deserialize",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
Assert.IsNotNull(deserializeMethod, "Deserialize method should exist");
|
||||
|
||||
var propertyNamesField = serializerType.GetField("PropertyNames",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
Assert.IsNotNull(propertyNamesField, "PropertyNames field should exist");
|
||||
|
||||
var propertyNames = propertyNamesField.GetValue(null) as string[];
|
||||
Assert.IsNotNull(propertyNames, "PropertyNames should not be null");
|
||||
Assert.AreEqual(3, propertyNames.Length, "SimpleGeneratedModel has 3 properties");
|
||||
|
||||
// Verify alphabetical order
|
||||
Assert.AreEqual("Age", propertyNames[0]);
|
||||
Assert.AreEqual("FirstName", propertyNames[1]);
|
||||
Assert.AreEqual("LastName", propertyNames[2]);
|
||||
Assert.IsNotNull(writerType, $"Generated writer type '{writerTypeName}' should exist");
|
||||
Assert.IsTrue(typeof(IGeneratedBinaryWriter).IsAssignableFrom(writerType),
|
||||
"Generated writer should implement IGeneratedBinaryWriter");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GeneratedSerializerPropertyNames_MatchRuntimeOrder()
|
||||
public void Serialization_WorksCorrectly_WithGeneratedWriterPresent()
|
||||
{
|
||||
// This test verifies that the generated property order matches the runtime serializer's order
|
||||
// This is critical for binary compatibility!
|
||||
|
||||
var type = typeof(GeneratedSerializerTestModel);
|
||||
|
||||
// Get generated property names
|
||||
var generatedTypeName = $"{type.FullName}_AcBinarySerializer";
|
||||
var serializerType = type.Assembly.GetType(generatedTypeName);
|
||||
Assert.IsNotNull(serializerType);
|
||||
|
||||
var propertyNamesField = serializerType.GetField("PropertyNames",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
|
||||
var generatedNames = propertyNamesField?.GetValue(null) as string[];
|
||||
Assert.IsNotNull(generatedNames);
|
||||
|
||||
// Get runtime property names using the same logic as TypeMetadataBase
|
||||
var runtimeProps = type.GetProperties(
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(p => p.CanRead && p.CanWrite && !p.GetIndexParameters().Any())
|
||||
.OrderBy(p => p.Name, StringComparer.Ordinal)
|
||||
.Select(p => p.Name)
|
||||
.ToArray();
|
||||
|
||||
// Assert they match
|
||||
CollectionAssert.AreEqual(runtimeProps, generatedNames,
|
||||
"Generated property names must match runtime property order for binary compatibility");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Serialization_WorksCorrectly_WithGeneratedSerializerPresent()
|
||||
{
|
||||
// This test ensures that regular serialization still works even when
|
||||
// generated serializers are present (they are not yet integrated into the hot path)
|
||||
|
||||
var original = new GeneratedSerializerTestModel
|
||||
{
|
||||
Id = 42,
|
||||
|
|
@ -108,11 +47,9 @@ public class GeneratedSerializerIntegrationTests
|
|||
BigNumber = 9999999999L
|
||||
};
|
||||
|
||||
// Serialize and deserialize using the regular path
|
||||
var bytes = AcBinarySerializer.Serialize(original, AcBinarySerializerOptions.WithoutReferenceHandling);
|
||||
var deserialized = AcBinaryDeserializer.Deserialize<GeneratedSerializerTestModel>(bytes);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual(original.Id, deserialized.Id);
|
||||
Assert.AreEqual(original.Name, deserialized.Name);
|
||||
|
|
@ -125,25 +62,73 @@ public class GeneratedSerializerIntegrationTests
|
|||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NestedType_GeneratedSerializer_IsFound()
|
||||
public void GeneratedWriter_PrimitiveClass_RoundTrip()
|
||||
{
|
||||
// Test that nested types (like QuickBenchmark.TestClassWithRepeatedValues)
|
||||
// have their generated serializers properly named and discoverable
|
||||
var original = new PrimitiveTestClass
|
||||
{
|
||||
IntValue = 42,
|
||||
StringValue = "TestName",
|
||||
BoolValue = true,
|
||||
DoubleValue = 3.14,
|
||||
DateTimeValue = new DateTime(2025, 1, 5, 10, 30, 0, DateTimeKind.Utc),
|
||||
GuidValue = Guid.NewGuid(),
|
||||
DecimalValue = 99.99m,
|
||||
LongValue = 9999999999L,
|
||||
FloatValue = 1.5f,
|
||||
ByteValue = 42,
|
||||
ShortValue = 123,
|
||||
EnumValue = TestStatus.Active,
|
||||
NullableInt = 7,
|
||||
NullableIntNull = null
|
||||
};
|
||||
|
||||
var type = typeof(QuickBenchmark.TestClassWithRepeatedValues);
|
||||
var ns = type.Namespace ?? "";
|
||||
var options = AcBinarySerializerOptions.FastMode;
|
||||
var bytes = AcBinarySerializer.Serialize(original, options);
|
||||
var deserialized = AcBinaryDeserializer.Deserialize<PrimitiveTestClass>(bytes, options);
|
||||
|
||||
// For nested types, the generated class is at namespace level with just the type name
|
||||
var simpleName = $"{(string.IsNullOrEmpty(ns) ? "" : ns + ".")}{type.Name}_AcBinarySerializer";
|
||||
var serializerType = type.Assembly.GetType(simpleName);
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual(original.IntValue, deserialized.IntValue);
|
||||
Assert.AreEqual(original.StringValue, deserialized.StringValue);
|
||||
Assert.AreEqual(original.BoolValue, deserialized.BoolValue);
|
||||
Assert.AreEqual(original.DoubleValue, deserialized.DoubleValue);
|
||||
Assert.AreEqual(original.DateTimeValue, deserialized.DateTimeValue);
|
||||
Assert.AreEqual(original.GuidValue, deserialized.GuidValue);
|
||||
Assert.AreEqual(original.DecimalValue, deserialized.DecimalValue);
|
||||
Assert.AreEqual(original.LongValue, deserialized.LongValue);
|
||||
Assert.AreEqual(original.NullableInt, deserialized.NullableInt);
|
||||
Assert.IsNull(deserialized.NullableIntNull);
|
||||
}
|
||||
|
||||
Assert.IsNotNull(serializerType,
|
||||
$"Generated serializer for nested type should be found at '{simpleName}'");
|
||||
[TestMethod]
|
||||
public void GeneratedWriter_ComplexHierarchy_RoundTrip()
|
||||
{
|
||||
TestDataFactory.ResetIdCounter();
|
||||
var sharedTag = TestDataFactory.CreateTag("SharedTag");
|
||||
var sharedUser = TestDataFactory.CreateUser("shareduser");
|
||||
|
||||
// Verify it has the expected methods
|
||||
Assert.IsNotNull(serializerType.GetMethod("Serialize",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static));
|
||||
Assert.IsNotNull(serializerType.GetMethod("Deserialize",
|
||||
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static));
|
||||
var order = TestDataFactory.CreateOrder(
|
||||
itemCount: 2,
|
||||
palletsPerItem: 2,
|
||||
measurementsPerPallet: 2,
|
||||
pointsPerMeasurement: 2,
|
||||
sharedTag: sharedTag,
|
||||
sharedUser: sharedUser);
|
||||
|
||||
var options = AcBinarySerializerOptions.FastMode;
|
||||
var bytes = AcBinarySerializer.Serialize(order, options);
|
||||
var deserialized = AcBinaryDeserializer.Deserialize<TestOrder>(bytes, options);
|
||||
|
||||
Assert.IsNotNull(deserialized);
|
||||
Assert.AreEqual(order.Id, deserialized.Id);
|
||||
Assert.AreEqual(order.OrderNumber, deserialized.OrderNumber);
|
||||
Assert.AreEqual(order.Status, deserialized.Status);
|
||||
Assert.AreEqual(order.TotalAmount, deserialized.TotalAmount);
|
||||
Assert.AreEqual(order.Items.Count, deserialized.Items.Count);
|
||||
|
||||
for (var i = 0; i < order.Items.Count; i++)
|
||||
{
|
||||
Assert.AreEqual(order.Items[i].Id, deserialized.Items[i].Id);
|
||||
Assert.AreEqual(order.Items[i].Pallets.Count, deserialized.Items[i].Pallets.Count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,44 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Registers a source-generated binary writer for the specified type.
|
||||
/// Once registered, WriteObject bypasses the runtime switch/delegate property loop
|
||||
/// and calls the generated writer directly — eliminating Func<>.Invoke() overhead.
|
||||
/// Call once at startup (e.g., in a static constructor or module initializer).
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to register the writer for.</typeparam>
|
||||
/// <param name="writer">The generated writer instance (typically a singleton).</param>
|
||||
internal static void RegisterGeneratedWriter<T>(IGeneratedBinaryWriter writer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
GeneratedWriterRegistry.Register(typeof(T), writer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a source-generated binary writer for the specified type.
|
||||
/// </summary>
|
||||
internal static void RegisterGeneratedWriter(Type type, IGeneratedBinaryWriter writer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
ArgumentNullException.ThrowIfNull(writer);
|
||||
GeneratedWriterRegistry.Register(type, writer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
|
||||
/// </summary>
|
||||
internal static class GeneratedWriterRegistry
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, IGeneratedBinaryWriter> Writers = new();
|
||||
|
||||
internal static void Register(Type type, IGeneratedBinaryWriter writer) => Writers[type] = writer;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static IGeneratedBinaryWriter? TryGet(Type type) =>
|
||||
Writers.TryGetValue(type, out var writer) ? writer : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize object to binary with default options.
|
||||
/// </summary>
|
||||
|
|
@ -429,6 +467,38 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Generated Writer Bridge Methods
|
||||
|
||||
/// <summary>
|
||||
/// Bridge for generated writers to call the runtime WriteValue for complex/collection properties.
|
||||
/// Generated writers handle primitives directly; complex types delegate here.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void WriteValueGenerated<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
WriteValue(value, type, context, depth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bridge for generated writers to call the runtime WriteString.
|
||||
/// Matches WritePropertyOrSkip String case exactly: null → PropertySkip, empty → StringEmpty.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void WriteStringGenerated<TOutput>(string? value, BinarySerializationContext<TOutput> context)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteString(value, context);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value Writing
|
||||
|
||||
private static void WriteValue<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> context, int depth)
|
||||
|
|
@ -1035,6 +1105,16 @@ public static partial class AcBinarySerializer
|
|||
var propCount = properties.Length;
|
||||
var hasPropertyFilter = context.HasPropertyFilter;
|
||||
|
||||
// Source-generated fast path: bypass the entire switch/delegate loop.
|
||||
// Only when no caching features are active (no string interning, no reference handling)
|
||||
// to avoid scan pass / write pass mismatch with interned strings and tracked references.
|
||||
var generatedWriter = wrapper.GeneratedWriter;
|
||||
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching)
|
||||
{
|
||||
generatedWriter.WriteProperties(value, context, nextDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.UseMetadata)
|
||||
{
|
||||
// Markerless loop: no extra branching per property for the common case.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for source-generated binary property writers.
|
||||
/// Implementations bypass the runtime switch/delegate property loop in WriteObject.
|
||||
/// Each generated writer handles all properties of a specific type using direct obj.Property access.
|
||||
///
|
||||
/// Performance gains over runtime path:
|
||||
/// - No Func<>.Invoke() delegate calls (~5-8ns/property saved)
|
||||
/// - No switch dispatch (~2-3ns/property saved)
|
||||
/// - No boxing for value type properties
|
||||
/// - Small per-type code (~300B) vs 27KB monolithic WriteObject — better ICache behavior
|
||||
/// </summary>
|
||||
internal interface IGeneratedBinaryWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes all properties of the given object to the serialization context.
|
||||
/// Called from WriteObject when a generated writer is available for the type.
|
||||
/// The implementation uses direct property access (obj.Id, obj.Name, etc.) instead of delegates.
|
||||
/// </summary>
|
||||
/// <param name="value">The object whose properties to write. Implementation casts to the concrete type.</param>
|
||||
/// <param name="context">The serialization context (owns buffer, position, options).</param>
|
||||
/// <param name="depth">Current depth in the object graph (for nested object serialization).</param>
|
||||
/// <typeparam name="TOutput">Output strategy (ArrayBinaryOutput or BufferWriterBinaryOutput).</typeparam>
|
||||
void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase;
|
||||
}
|
||||
|
|
@ -58,6 +58,13 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
/// </summary>
|
||||
internal TypeMetadataWrapper<TMetadata>?[]? PropertyTypeWrappers;
|
||||
|
||||
/// <summary>
|
||||
/// Source-generated binary writer for this type. Bypasses the runtime switch/delegate loop.
|
||||
/// Set via AcBinarySerializer.RegisterGeneratedWriter. Null = use runtime path.
|
||||
/// Checked once per object in WriteObject (not per property).
|
||||
/// </summary>
|
||||
internal IGeneratedBinaryWriter? GeneratedWriter;
|
||||
|
||||
/// <summary>
|
||||
/// Options-filtered subset of metadata.ReferenceProperties for the scan pass.
|
||||
/// Built lazily on first scan pass call, stable during session, cleared in ResetTracking.
|
||||
|
|
@ -135,6 +142,9 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
// Pre-allocate PropertyTypeWrappers — eliminates null/resize checks from hot path
|
||||
if (metadata.ComplexPropertyCount > 0)
|
||||
PropertyTypeWrappers = new TypeMetadataWrapper<TMetadata>?[metadata.ComplexPropertyCount];
|
||||
|
||||
// Lookup generated writer from registry (once per wrapper creation, not per serialization)
|
||||
GeneratedWriter = AcBinarySerializer.GeneratedWriterRegistry.TryGet(metadata.SourceType);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue