Refactor deserializer marker handling; add UseGeneratedCode opt

Refactored AcBinaryDeserializer to read the type code marker byte only once per property, eliminating redundant PeekByte/ReadByte calls and improving efficiency. Updated all property population branches to use the already-consumed type code. Adjusted handling of nested complex objects to rewind the marker byte when needed. Modified TryReadAndSetTypedValue to assume the marker is already consumed, removing unnecessary reads. Exception messages now report the actual type code read.

Added UseGeneratedCode option (default true) to AcBinarySerializerOptions and exposed it in the serialization context. The generated code fast path is now gated by this option, allowing users to enable or disable source-generated serialization. These changes improve deserialization performance, code clarity, and configurability.
This commit is contained in:
Loretta 2026-02-15 09:50:16 +01:00
parent 12b3244aa3
commit e50dca93fa
5 changed files with 74 additions and 93 deletions

View File

@ -169,20 +169,21 @@ public static partial class AcBinaryDeserializer
{ {
var context = state.Context; var context = state.Context;
var target = state.Target; var target = state.Target;
var peekCode = context.PeekByte();
// Nincs megfelelő target property → skip // Nincs megfelelő target property → skip (SkipValue reads its own marker byte)
if (propInfo == null) if (propInfo == null)
{ {
SkipValue(context, state.Metadata); SkipValue(context, state.Metadata);
return; return;
} }
// Skip marker - property has default/null value // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks.
if (peekCode == BinaryTypeCode.PropertySkip) // All branches below receive the already-consumed typeCode.
{ var typeCode = context.ReadByte();
context.ReadByte(); // consume Skip marker
// Skip marker - property has default/null value
if (typeCode == BinaryTypeCode.PropertySkip)
{
// Populate mode: overwrite with default (existing object may have non-default values) // Populate mode: overwrite with default (existing object may have non-default values)
// Deserialize mode: skip write (new object already has defaults from CreateInstance) // Deserialize mode: skip write (new object already has defaults from CreateInstance)
if (!state.SkipDefaultWrite) if (!state.SkipDefaultWrite)
@ -193,9 +194,8 @@ public static partial class AcBinaryDeserializer
} }
// Null values - always set // Null values - always set
if (peekCode == BinaryTypeCode.Null) if (typeCode == BinaryTypeCode.Null)
{ {
context.ReadByte(); // consume Null marker
propInfo.SetValue(target, null); propInfo.SetValue(target, null);
return; return;
} }
@ -203,13 +203,11 @@ public static partial class AcBinaryDeserializer
var nextDepth = state.NextDepth; var nextDepth = state.NextDepth;
// Handle collections // Handle collections
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) if (typeCode == BinaryTypeCode.Array && propInfo.IsCollection)
{ {
var existingCollection = propInfo.GetValue(target); var existingCollection = propInfo.GetValue(target);
if (existingCollection is IList existingList) if (existingCollection is IList existingList)
{ {
context.ReadByte(); // consume Array marker
// Merge mode with IId collection: use merge logic // Merge mode with IId collection: use merge logic
if (state.IsMergeMode && propInfo.IsIIdCollection) if (state.IsMergeMode && propInfo.IsIIdCollection)
{ {
@ -225,12 +223,13 @@ public static partial class AcBinaryDeserializer
} }
// Handle nested complex objects - reuse existing if available // Handle nested complex objects - reuse existing if available
if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) if ((typeCode == BinaryTypeCode.Object || typeCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType)
{ {
var existingObj = propInfo.GetValue(target); var existingObj = propInfo.GetValue(target);
if (existingObj != null) if (existingObj != null)
{ {
// ReadValue kezeli mindkét markert // Marker already consumed → rewind so ReadValue can read it
context._position--;
var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth); var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth);
if (nestedValue != null) if (nestedValue != null)
{ {
@ -246,25 +245,28 @@ public static partial class AcBinaryDeserializer
} }
// Default: read value and set (for primitives, strings, new objects) // Default: read value and set (for primitives, strings, new objects)
var positionBeforeRead = context.Position; var positionBeforeRead = context.Position - 1; // marker already consumed
try try
{ {
// Use typed setters for primitives and strings to avoid ReadValue dispatch // Use typed setters for primitives and strings to avoid ReadValue dispatch.
// typeCode is already consumed — TryReadAndSetTypedValue skips its internal ReadByte.
if (propInfo.AccessorType != PropertyAccessorType.Object && if (propInfo.AccessorType != PropertyAccessorType.Object &&
TryReadAndSetTypedValue(context, target, propInfo, peekCode)) TryReadAndSetTypedValue(context, target, propInfo, typeCode))
return; return;
// Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup // Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup
var complexIdx = propInfo.ComplexPropertyIndex; var complexIdx = propInfo.ComplexPropertyIndex;
if (complexIdx >= 0 && peekCode == BinaryTypeCode.Object) if (complexIdx >= 0 && typeCode == BinaryTypeCode.Object)
{ {
context.ReadByte(); // consume Object marker // Marker already consumed — go straight to ReadObjectCoreWithWrapper
var propWrapper = ResolvePropertyWrapper(state.ParentWrapper, complexIdx, propInfo.PropertyType, context); var propWrapper = ResolvePropertyWrapper(state.ParentWrapper, complexIdx, propInfo.PropertyType, context);
var value = ReadObjectCoreWithWrapper(context, propWrapper, nextDepth, cacheIndex: -1); var value = ReadObjectCoreWithWrapper(context, propWrapper, nextDepth, cacheIndex: -1);
propInfo.SetValue(target, value); propInfo.SetValue(target, value);
} }
else else
{ {
// Marker already consumed → rewind so ReadValue can read it
context._position--;
var value = ReadValue(context, propInfo.PropertyType, nextDepth); var value = ReadValue(context, propInfo.PropertyType, nextDepth);
propInfo.SetValue(target, value); propInfo.SetValue(target, value);
} }
@ -275,7 +277,7 @@ public static partial class AcBinaryDeserializer
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(
$"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " + $"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " +
$"Expected type: '{propInfo.PropertyType.FullName}'. " + $"Expected type: '{propInfo.PropertyType.FullName}'. " +
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + $"TypeCode read: {typeCode} (0x{typeCode:X2}). " +
$"Position before read: {positionBeforeRead}, current: {context.Position}. " + $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
$"Depth: {state.Depth}. " + $"Depth: {state.Depth}. " +
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +

View File

@ -608,194 +608,173 @@ public static partial class AcBinaryDeserializer
/// <summary> /// <summary>
/// Tries to read and set a primitive value directly using typed setters to avoid boxing. /// Tries to read and set a primitive value directly using typed setters to avoid boxing.
/// The type code marker byte is already consumed by the caller.
/// Returns true if handled, false if should fall back to generic path. /// Returns true if handled, false if should fall back to generic path.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryReadAndSetTypedValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo, byte peekCode) private static bool TryReadAndSetTypedValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo, byte typeCode)
where TInput : struct, IBinaryInputBase where TInput : struct, IBinaryInputBase
{ {
// Only handle if we have a typed setter // Only handle if we have a typed setter
if (propInfo.AccessorType == PropertyAccessorType.Object) if (propInfo.AccessorType == PropertyAccessorType.Object)
return false; return false;
// Handle based on property setter type and incoming data type // Handle based on property setter type and incoming data type.
// The marker byte (typeCode) is already consumed — no ReadByte() needed.
switch (propInfo.AccessorType) switch (propInfo.AccessorType)
{ {
case PropertyAccessorType.Int32: case PropertyAccessorType.Int32:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.Int32) if (typeCode == BinaryTypeCode.Int32)
{ {
context.ReadByte();
propInfo.SetInt32(target, context.ReadVarInt()); propInfo.SetInt32(target, context.ReadVarInt());
return true; return true;
} }
break; break;
case PropertyAccessorType.Int64: case PropertyAccessorType.Int64:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.Int32) if (typeCode == BinaryTypeCode.Int32)
{ {
context.ReadByte();
propInfo.SetInt64(target, context.ReadVarInt()); propInfo.SetInt64(target, context.ReadVarInt());
return true; return true;
} }
if (peekCode == BinaryTypeCode.Int64) if (typeCode == BinaryTypeCode.Int64)
{ {
context.ReadByte();
propInfo.SetInt64(target, context.ReadVarLong()); propInfo.SetInt64(target, context.ReadVarLong());
return true; return true;
} }
break; break;
case PropertyAccessorType.Boolean: case PropertyAccessorType.Boolean:
if (peekCode == BinaryTypeCode.True) if (typeCode == BinaryTypeCode.True)
{ {
context.ReadByte();
propInfo.SetBoolean(target, true); propInfo.SetBoolean(target, true);
return true; return true;
} }
if (peekCode == BinaryTypeCode.False) if (typeCode == BinaryTypeCode.False)
{ {
context.ReadByte();
propInfo.SetBoolean(target, false); propInfo.SetBoolean(target, false);
return true; return true;
} }
break; break;
case PropertyAccessorType.Double: case PropertyAccessorType.Double:
if (peekCode == BinaryTypeCode.Float64) if (typeCode == BinaryTypeCode.Float64)
{ {
context.ReadByte();
propInfo.SetDouble(target, context.ReadDoubleUnsafe()); propInfo.SetDouble(target, context.ReadDoubleUnsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.Single: case PropertyAccessorType.Single:
if (peekCode == BinaryTypeCode.Float32) if (typeCode == BinaryTypeCode.Float32)
{ {
context.ReadByte();
propInfo.SetSingle(target, context.ReadSingleUnsafe()); propInfo.SetSingle(target, context.ReadSingleUnsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.Decimal: case PropertyAccessorType.Decimal:
if (peekCode == BinaryTypeCode.Decimal) if (typeCode == BinaryTypeCode.Decimal)
{ {
context.ReadByte();
propInfo.SetDecimal(target, context.ReadDecimalUnsafe()); propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.DateTime: case PropertyAccessorType.DateTime:
if (peekCode == BinaryTypeCode.DateTime) if (typeCode == BinaryTypeCode.DateTime)
{ {
context.ReadByte();
propInfo.SetDateTime(target, context.ReadDateTimeUnsafe()); propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.Guid: case PropertyAccessorType.Guid:
if (peekCode == BinaryTypeCode.Guid) if (typeCode == BinaryTypeCode.Guid)
{ {
context.ReadByte();
propInfo.SetGuid(target, context.ReadGuidUnsafe()); propInfo.SetGuid(target, context.ReadGuidUnsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.Byte: case PropertyAccessorType.Byte:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.UInt8) if (typeCode == BinaryTypeCode.UInt8)
{ {
context.ReadByte();
propInfo.SetByte(target, context.ReadByte()); propInfo.SetByte(target, context.ReadByte());
return true; return true;
} }
break; break;
case PropertyAccessorType.Int16: case PropertyAccessorType.Int16:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.Int16) if (typeCode == BinaryTypeCode.Int16)
{ {
context.ReadByte();
propInfo.SetInt16(target, context.ReadInt16Unsafe()); propInfo.SetInt16(target, context.ReadInt16Unsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.UInt16: case PropertyAccessorType.UInt16:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.UInt16) if (typeCode == BinaryTypeCode.UInt16)
{ {
context.ReadByte();
propInfo.SetUInt16(target, context.ReadUInt16Unsafe()); propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
return true; return true;
} }
break; break;
case PropertyAccessorType.UInt32: case PropertyAccessorType.UInt32:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.UInt32) if (typeCode == BinaryTypeCode.UInt32)
{ {
context.ReadByte();
propInfo.SetUInt32(target, context.ReadVarUInt()); propInfo.SetUInt32(target, context.ReadVarUInt());
return true; return true;
} }
break; break;
case PropertyAccessorType.UInt64: case PropertyAccessorType.UInt64:
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
if (peekCode == BinaryTypeCode.UInt64) if (typeCode == BinaryTypeCode.UInt64)
{ {
context.ReadByte();
propInfo.SetUInt64(target, context.ReadVarULong()); propInfo.SetUInt64(target, context.ReadVarULong());
return true; return true;
} }
break; break;
case PropertyAccessorType.Enum: case PropertyAccessorType.Enum:
if (peekCode == BinaryTypeCode.Enum) if (typeCode == BinaryTypeCode.Enum)
{ {
context.ReadByte();
var enumByte = context.ReadByte(); var enumByte = context.ReadByte();
int enumValue; int enumValue;
if (BinaryTypeCode.IsTinyInt(enumByte)) if (BinaryTypeCode.IsTinyInt(enumByte))
@ -808,43 +787,37 @@ public static partial class AcBinaryDeserializer
return true; return true;
} }
// Enum can also be encoded as TinyInt directly // Enum can also be encoded as TinyInt directly
if (BinaryTypeCode.IsTinyInt(peekCode)) if (BinaryTypeCode.IsTinyInt(typeCode))
{ {
context.ReadByte(); propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode));
return true; return true;
} }
break; break;
case PropertyAccessorType.String: case PropertyAccessorType.String:
if (BinaryTypeCode.IsFixStr(peekCode)) if (BinaryTypeCode.IsFixStr(typeCode))
{ {
context.ReadByte(); var length = BinaryTypeCode.DecodeFixStrLength(typeCode);
var length = BinaryTypeCode.DecodeFixStrLength(peekCode);
propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length)); propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length));
return true; return true;
} }
if (peekCode == BinaryTypeCode.String) if (typeCode == BinaryTypeCode.String)
{ {
context.ReadByte();
propInfo.SetValue(target, ReadPlainString(context)); propInfo.SetValue(target, ReadPlainString(context));
return true; return true;
} }
if (peekCode == BinaryTypeCode.StringEmpty) if (typeCode == BinaryTypeCode.StringEmpty)
{ {
context.ReadByte();
propInfo.SetValue(target, string.Empty); propInfo.SetValue(target, string.Empty);
return true; return true;
} }
if (peekCode == BinaryTypeCode.StringInterned) if (typeCode == BinaryTypeCode.StringInterned)
{ {
context.ReadByte();
propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt())); propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
return true; return true;
} }
if (peekCode == BinaryTypeCode.StringInternFirst) if (typeCode == BinaryTypeCode.StringInternFirst)
{ {
context.ReadByte();
propInfo.SetValue(target, ReadAndRegisterInternedString(context)); propInfo.SetValue(target, ReadAndRegisterInternedString(context));
return true; return true;
} }

View File

@ -133,6 +133,7 @@ public static partial class AcBinarySerializer
/// </summary> /// </summary>
public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None;
public bool UseMetadata => Options.UseMetadata; public bool UseMetadata => Options.UseMetadata;
public bool UseGeneratedCode => Options.UseGeneratedCode;
public byte MinStringInternLength => Options.MinStringInternLength; public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength;
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter; public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;

View File

@ -1109,13 +1109,16 @@ public static partial class AcBinarySerializer
// Source-generated fast path: bypass the entire switch/delegate loop. // Source-generated fast path: bypass the entire switch/delegate loop.
// Only when no caching features are active (no string interning, no reference handling) // 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. // to avoid scan pass / write pass mismatch with interned strings and tracked references.
var generatedWriter = wrapper.GeneratedWriter; if (context.UseGeneratedCode)
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching)
{ {
generatedWriter.WriteProperties(value, context, nextDepth); var generatedWriter = wrapper.GeneratedWriter;
return; if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching)
{
generatedWriter.WriteProperties(value, context, nextDepth);
return;
}
} }
if (!context.UseMetadata) if (!context.UseMetadata)
{ {
// Markerless loop: no extra branching per property for the common case. // Markerless loop: no extra branching per property for the common case.

View File

@ -84,6 +84,8 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
/// </summary> /// </summary>
public bool UseMetadata { get; set; } = false; public bool UseMetadata { get; set; } = false;
public bool UseGeneratedCode { get; set; } = true;
/// <summary> /// <summary>
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
/// Throws exception if FNV-1a hash collision is detected between property names of the same type. /// Throws exception if FNV-1a hash collision is detected between property names of the same type.