From e50dca93fa287a57e8c42cc448aa8207e79f7e6e Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 15 Feb 2026 09:50:16 +0100 Subject: [PATCH] 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. --- .../Binaries/AcBinaryDeserializer.Populate.cs | 40 ++++--- .../Binaries/AcBinaryDeserializer.cs | 111 +++++++----------- ...rySerializer.BinarySerializationContext.cs | 1 + .../Binaries/AcBinarySerializer.cs | 13 +- .../Binaries/AcBinarySerializerOptions.cs | 2 + 5 files changed, 74 insertions(+), 93 deletions(-) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 14372d0..f291741 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -169,20 +169,21 @@ public static partial class AcBinaryDeserializer { var context = state.Context; 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) { SkipValue(context, state.Metadata); return; } - // Skip marker - property has default/null value - if (peekCode == BinaryTypeCode.PropertySkip) - { - context.ReadByte(); // consume Skip marker + // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks. + // All branches below receive the already-consumed typeCode. + var typeCode = context.ReadByte(); + // Skip marker - property has default/null value + if (typeCode == BinaryTypeCode.PropertySkip) + { // Populate mode: overwrite with default (existing object may have non-default values) // Deserialize mode: skip write (new object already has defaults from CreateInstance) if (!state.SkipDefaultWrite) @@ -193,9 +194,8 @@ public static partial class AcBinaryDeserializer } // Null values - always set - if (peekCode == BinaryTypeCode.Null) + if (typeCode == BinaryTypeCode.Null) { - context.ReadByte(); // consume Null marker propInfo.SetValue(target, null); return; } @@ -203,13 +203,11 @@ public static partial class AcBinaryDeserializer var nextDepth = state.NextDepth; // Handle collections - if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) + if (typeCode == BinaryTypeCode.Array && propInfo.IsCollection) { var existingCollection = propInfo.GetValue(target); if (existingCollection is IList existingList) { - context.ReadByte(); // consume Array marker - // Merge mode with IId collection: use merge logic if (state.IsMergeMode && propInfo.IsIIdCollection) { @@ -225,12 +223,13 @@ public static partial class AcBinaryDeserializer } // 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); 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); if (nestedValue != null) { @@ -246,25 +245,28 @@ public static partial class AcBinaryDeserializer } // Default: read value and set (for primitives, strings, new objects) - var positionBeforeRead = context.Position; + var positionBeforeRead = context.Position - 1; // marker already consumed 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 && - TryReadAndSetTypedValue(context, target, propInfo, peekCode)) + TryReadAndSetTypedValue(context, target, propInfo, typeCode)) return; // Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup 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 value = ReadObjectCoreWithWrapper(context, propWrapper, nextDepth, cacheIndex: -1); propInfo.SetValue(target, value); } else { + // Marker already consumed → rewind so ReadValue can read it + context._position--; var value = ReadValue(context, propInfo.PropertyType, nextDepth); propInfo.SetValue(target, value); } @@ -275,7 +277,7 @@ public static partial class AcBinaryDeserializer throw new AcBinaryDeserializationException( $"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " + $"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}. " + $"Depth: {state.Depth}. " + $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 3dc462b..9f0c8fe 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -608,194 +608,173 @@ public static partial class AcBinaryDeserializer /// /// 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. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte peekCode) + private static bool TryReadAndSetTypedValue(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo, byte typeCode) where TInput : struct, IBinaryInputBase { // Only handle if we have a typed setter if (propInfo.AccessorType == PropertyAccessorType.Object) 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) { case PropertyAccessorType.Int32: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.Int32) + if (typeCode == BinaryTypeCode.Int32) { - context.ReadByte(); propInfo.SetInt32(target, context.ReadVarInt()); return true; } break; case PropertyAccessorType.Int64: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.Int32) + if (typeCode == BinaryTypeCode.Int32) { - context.ReadByte(); propInfo.SetInt64(target, context.ReadVarInt()); return true; } - if (peekCode == BinaryTypeCode.Int64) + if (typeCode == BinaryTypeCode.Int64) { - context.ReadByte(); propInfo.SetInt64(target, context.ReadVarLong()); return true; } break; case PropertyAccessorType.Boolean: - if (peekCode == BinaryTypeCode.True) + if (typeCode == BinaryTypeCode.True) { - context.ReadByte(); propInfo.SetBoolean(target, true); return true; } - if (peekCode == BinaryTypeCode.False) + if (typeCode == BinaryTypeCode.False) { - context.ReadByte(); propInfo.SetBoolean(target, false); return true; } break; case PropertyAccessorType.Double: - if (peekCode == BinaryTypeCode.Float64) + if (typeCode == BinaryTypeCode.Float64) { - context.ReadByte(); propInfo.SetDouble(target, context.ReadDoubleUnsafe()); return true; } break; case PropertyAccessorType.Single: - if (peekCode == BinaryTypeCode.Float32) + if (typeCode == BinaryTypeCode.Float32) { - context.ReadByte(); propInfo.SetSingle(target, context.ReadSingleUnsafe()); return true; } break; case PropertyAccessorType.Decimal: - if (peekCode == BinaryTypeCode.Decimal) + if (typeCode == BinaryTypeCode.Decimal) { - context.ReadByte(); propInfo.SetDecimal(target, context.ReadDecimalUnsafe()); return true; } break; case PropertyAccessorType.DateTime: - if (peekCode == BinaryTypeCode.DateTime) + if (typeCode == BinaryTypeCode.DateTime) { - context.ReadByte(); propInfo.SetDateTime(target, context.ReadDateTimeUnsafe()); return true; } break; case PropertyAccessorType.Guid: - if (peekCode == BinaryTypeCode.Guid) + if (typeCode == BinaryTypeCode.Guid) { - context.ReadByte(); propInfo.SetGuid(target, context.ReadGuidUnsafe()); return true; } break; case PropertyAccessorType.Byte: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt8) + if (typeCode == BinaryTypeCode.UInt8) { - context.ReadByte(); propInfo.SetByte(target, context.ReadByte()); return true; } break; case PropertyAccessorType.Int16: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.Int16) + if (typeCode == BinaryTypeCode.Int16) { - context.ReadByte(); propInfo.SetInt16(target, context.ReadInt16Unsafe()); return true; } break; case PropertyAccessorType.UInt16: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt16) + if (typeCode == BinaryTypeCode.UInt16) { - context.ReadByte(); propInfo.SetUInt16(target, context.ReadUInt16Unsafe()); return true; } break; case PropertyAccessorType.UInt32: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt32) + if (typeCode == BinaryTypeCode.UInt32) { - context.ReadByte(); propInfo.SetUInt32(target, context.ReadVarUInt()); return true; } break; case PropertyAccessorType.UInt64: - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } - if (peekCode == BinaryTypeCode.UInt64) + if (typeCode == BinaryTypeCode.UInt64) { - context.ReadByte(); propInfo.SetUInt64(target, context.ReadVarULong()); return true; } break; case PropertyAccessorType.Enum: - if (peekCode == BinaryTypeCode.Enum) + if (typeCode == BinaryTypeCode.Enum) { - context.ReadByte(); var enumByte = context.ReadByte(); int enumValue; if (BinaryTypeCode.IsTinyInt(enumByte)) @@ -808,43 +787,37 @@ public static partial class AcBinaryDeserializer return true; } // Enum can also be encoded as TinyInt directly - if (BinaryTypeCode.IsTinyInt(peekCode)) + if (BinaryTypeCode.IsTinyInt(typeCode)) { - context.ReadByte(); - propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(peekCode)); + propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode)); return true; } break; case PropertyAccessorType.String: - if (BinaryTypeCode.IsFixStr(peekCode)) + if (BinaryTypeCode.IsFixStr(typeCode)) { - context.ReadByte(); - var length = BinaryTypeCode.DecodeFixStrLength(peekCode); + var length = BinaryTypeCode.DecodeFixStrLength(typeCode); propInfo.SetValue(target, length == 0 ? string.Empty : context.ReadStringUtf8(length)); return true; } - if (peekCode == BinaryTypeCode.String) + if (typeCode == BinaryTypeCode.String) { - context.ReadByte(); propInfo.SetValue(target, ReadPlainString(context)); return true; } - if (peekCode == BinaryTypeCode.StringEmpty) + if (typeCode == BinaryTypeCode.StringEmpty) { - context.ReadByte(); propInfo.SetValue(target, string.Empty); return true; } - if (peekCode == BinaryTypeCode.StringInterned) + if (typeCode == BinaryTypeCode.StringInterned) { - context.ReadByte(); propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt())); return true; } - if (peekCode == BinaryTypeCode.StringInternFirst) + if (typeCode == BinaryTypeCode.StringInternFirst) { - context.ReadByte(); propInfo.SetValue(target, ReadAndRegisterInternedString(context)); return true; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index f1f0a27..ca66ad8 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -133,6 +133,7 @@ public static partial class AcBinarySerializer /// public bool HasCaching => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; public bool UseMetadata => Options.UseMetadata; + public bool UseGeneratedCode => Options.UseGeneratedCode; public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter; diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 803a1b2..1429b62 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -1109,13 +1109,16 @@ public static partial class AcBinarySerializer // 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) + if (context.UseGeneratedCode) { - generatedWriter.WriteProperties(value, context, nextDepth); - return; + 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. diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index f52489b..a41a6df 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -84,6 +84,8 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// public bool UseMetadata { get; set; } = false; + public bool UseGeneratedCode { get; set; } = true; + /// /// 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.