diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 8d47eed..5984d4f 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -77,108 +77,196 @@ public static partial class AcBinaryDeserializer // Non-UseMetadata: properties.Length a target property-k száma (source == target) var propCount = cacheMap?.Length ?? properties.Length; - for (int i = 0; i < propCount; i++) + if (!context.HasMetadata) { - var propInfo = cacheMap != null ? cacheMap[i] : properties[i]; - - var peekCode = context.PeekByte(); - - // Nincs megfelelÅ‘ target property → skip - if (propInfo == null) + // Markerless loop: properties with ExpectedTypeCode read raw values directly. + // Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path. + for (int i = 0; i < propCount; i++) { - SkipValue(ref context, metadata); - continue; - } + var propInfo = properties[i]; - // Skip marker - property has default/null value - if (peekCode == BinaryTypeCode.PropertySkip) - { - context.ReadByte(); // consume Skip marker - - // Populate mode: overwrite with default (existing object may have non-default values) - // Deserialize mode: skip write (new object already has defaults from CreateInstance) - if (!skipDefaultWrite) + if (propInfo.ExpectedTypeCode.HasValue) { - SetPropertyToDefault(target, propInfo); - } - continue; - } - - // Null values - always set - if (peekCode == BinaryTypeCode.Null) - { - context.ReadByte(); // consume Null marker - propInfo.SetValue(target, null); - continue; - } - - // Handle collections - if (peekCode == 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 (isMergeMode && propInfo.IsIIdCollection) - { - MergeIIdCollection(ref context, existingList, propInfo, nextDepth); - } - else - { - // Normal populate: replace collection contents - PopulateListOptimized(ref context, existingList, propInfo, nextDepth); - } + ReadAndSetMarkerlessValue(ref context, target, propInfo); continue; } - } - // Handle nested complex objects - reuse existing if available - if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) + // Non-markerless properties: standard marker-based read + PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth); + } + } + else + { + // UseMetadata=true loop — UNCHANGED, zero extra overhead + for (int i = 0; i < propCount; i++) { - var existingObj = propInfo.GetValue(target); - if (existingObj != null) + var propInfo = cacheMap != null ? cacheMap[i] : properties[i]; + + PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth); + } + } + } + + /// + /// Standard marker-based property read. Extracted to avoid duplicating logic in both loops. + /// + private static void PopulatePropertyWithMarker( + ref BinaryDeserializationContext context, + object target, + BinaryPropertySetterBase? propInfo, + BinaryDeserializeTypeMetadata metadata, + int nextDepth, + bool isMergeMode, + bool skipDefaultWrite, + int propertyIndex, + int depth) + { + var peekCode = context.PeekByte(); + + // Nincs megfelelÅ‘ target property → skip + if (propInfo == null) + { + SkipValue(ref context, metadata); + return; + } + + // Skip marker - property has default/null value + if (peekCode == BinaryTypeCode.PropertySkip) + { + context.ReadByte(); // consume Skip marker + + // Populate mode: overwrite with default (existing object may have non-default values) + // Deserialize mode: skip write (new object already has defaults from CreateInstance) + if (!skipDefaultWrite) + { + SetPropertyToDefault(target, propInfo); + } + return; + } + + // Null values - always set + if (peekCode == BinaryTypeCode.Null) + { + context.ReadByte(); // consume Null marker + propInfo.SetValue(target, null); + return; + } + + // Handle collections + if (peekCode == 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 (isMergeMode && propInfo.IsIIdCollection) { - // ReadValue kezeli mindkét markert - var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth); - if (nestedValue != null) - { - var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata; - CopyProperties(nestedValue, existingObj, nestedMeta); - } - continue; + MergeIIdCollection(ref context, existingList, propInfo, nextDepth); } + else + { + // Normal populate: replace collection contents + PopulateListOptimized(ref context, existingList, propInfo, nextDepth); + } + return; } + } - // Default: read value and set (for primitives, strings, new objects) - var positionBeforeRead = context.Position; - try + // Handle nested complex objects - reuse existing if available + if ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) { - // Use typed setters for primitives and strings to avoid ReadValue dispatch - if (propInfo.AccessorType != PropertyAccessorType.Object && - TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) - continue; + // ReadValue kezeli mindkét markert + var nestedValue = ReadValue(ref context, propInfo.PropertyType, nextDepth); + if (nestedValue != null) + { + var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata; + CopyProperties(nestedValue, existingObj, nestedMeta); + } + return; + } + } - var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); - propInfo.SetValue(target, value); - } - catch (InvalidCastException ex) - { - var targetType = target.GetType(); - throw new AcBinaryDeserializationException( - $"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetType.Name}'. " + - $"Expected type: '{propInfo.PropertyType.FullName}'. " + - $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + - $"Position before read: {positionBeforeRead}, current: {context.Position}. " + - $"Depth: {depth}. " + - $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + - $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " + - $"Error: {ex.Message}", - positionBeforeRead, - propInfo.PropertyType, - ex); - } + // Default: read value and set (for primitives, strings, new objects) + var positionBeforeRead = context.Position; + try + { + // Use typed setters for primitives and strings to avoid ReadValue dispatch + if (propInfo.AccessorType != PropertyAccessorType.Object && + TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) + return; + + var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); + propInfo.SetValue(target, value); + } + catch (InvalidCastException ex) + { + var targetType = target.GetType(); + 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}). " + + $"Position before read: {positionBeforeRead}, current: {context.Position}. " + + $"Depth: {depth}. " + + $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " + + $"All target properties: [{string.Join(", ", metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " + + $"Error: {ex.Message}", + positionBeforeRead, + propInfo.PropertyType, + ex); + } + } + + /// + /// Reads a raw value without type marker from stream (markerless mode, UseMetadata=false). + /// The property's type is known from metadata — no type code in the stream. + /// Only called for non-nullable value types with ExpectedTypeCode set. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadAndSetMarkerlessValue(ref BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo) + { + switch (propInfo.AccessorType) + { + case PropertyAccessorType.Int32: + propInfo.SetInt32(target, context.ReadVarInt()); + return; + case PropertyAccessorType.Int64: + propInfo.SetInt64(target, context.ReadVarLong()); + return; + case PropertyAccessorType.Double: + propInfo.SetDouble(target, context.ReadDoubleUnsafe()); + return; + case PropertyAccessorType.Single: + propInfo.SetSingle(target, context.ReadSingleUnsafe()); + return; + case PropertyAccessorType.Decimal: + propInfo.SetDecimal(target, context.ReadDecimalUnsafe()); + return; + case PropertyAccessorType.DateTime: + propInfo.SetDateTime(target, context.ReadDateTimeUnsafe()); + return; + case PropertyAccessorType.Guid: + propInfo.SetGuid(target, context.ReadGuidUnsafe()); + return; + case PropertyAccessorType.Byte: + propInfo.SetByte(target, context.ReadByte()); + return; + case PropertyAccessorType.Int16: + propInfo.SetInt16(target, context.ReadInt16Unsafe()); + return; + case PropertyAccessorType.UInt16: + propInfo.SetUInt16(target, context.ReadUInt16Unsafe()); + return; + case PropertyAccessorType.UInt32: + propInfo.SetUInt32(target, context.ReadVarUInt()); + return; + case PropertyAccessorType.UInt64: + propInfo.SetUInt64(target, context.ReadVarULong()); + return; } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 20f8394..aa4964e 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -944,27 +944,46 @@ public static partial class AcBinarySerializer var nextDepth = depth + 1; var properties = metadata.Properties; var propCount = properties.Length; - - // Single-pass serialization with SKIP markers - // - No property count needed (fixed property order) - // - No property indices needed (sequential order) - // - Single getter call per property - // - Write value OR skip marker in one operation var hasPropertyFilter = context.HasPropertyFilter; - for (var i = 0; i < propCount; i++) + if (!context.UseMetadata) { - var prop = properties[i]; - - // Skip if filter says no - write skip marker - if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) + // Markerless loop: no extra branching per property for the common case. + // Properties with ExpectedTypeCode write raw values (no type marker, no skip). + // Properties without ExpectedTypeCode (bool, enum, string, object) use the standard path. + for (var i = 0; i < propCount; i++) { - context.WriteByte(BinaryTypeCode.PropertySkip); - continue; + var prop = properties[i]; + + if (prop.ExpectedTypeCode.HasValue) + { + WritePropertyMarkerless(value, prop, context); + } + else if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) + { + context.WriteByte(BinaryTypeCode.PropertySkip); + } + else + { + WritePropertyOrSkip(value, prop, context, nextDepth); + } + } + } + else + { + // UseMetadata=true loop — UNCHANGED, zero extra overhead + for (var i = 0; i < propCount; i++) + { + var prop = properties[i]; + + if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop)) + { + context.WriteByte(BinaryTypeCode.PropertySkip); + continue; + } + + WritePropertyOrSkip(value, prop, context, nextDepth); } - - // Write property value OR skip marker (single operation, single getter call) - WritePropertyOrSkip(value, prop, context, nextDepth); } } @@ -1283,6 +1302,55 @@ public static partial class AcBinarySerializer } } + /// + /// Writes a property value without type marker byte (markerless mode, UseMetadata=false). + /// All values are written including defaults — no PropertySkip markers. + /// Only called for non-nullable value types with ExpectedTypeCode set. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WritePropertyMarkerless(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context) + { + switch (prop.AccessorType) + { + case PropertyAccessorType.Int32: + context.WriteVarInt(prop.GetInt32(obj)); + return; + case PropertyAccessorType.Int64: + context.WriteVarLong(prop.GetInt64(obj)); + return; + case PropertyAccessorType.Double: + context.WriteRaw(prop.GetDouble(obj)); + return; + case PropertyAccessorType.Single: + context.WriteRaw(prop.GetSingle(obj)); + return; + case PropertyAccessorType.Decimal: + context.WriteDecimalBits(prop.GetDecimal(obj)); + return; + case PropertyAccessorType.DateTime: + context.WriteDateTimeBits(prop.GetDateTime(obj)); + return; + case PropertyAccessorType.Guid: + context.WriteGuidBits(prop.GetGuid(obj)); + return; + case PropertyAccessorType.Byte: + context.WriteByte(prop.GetByte(obj)); + return; + case PropertyAccessorType.Int16: + context.WriteRaw(prop.GetInt16(obj)); + return; + case PropertyAccessorType.UInt16: + context.WriteRaw(prop.GetUInt16(obj)); + return; + case PropertyAccessorType.UInt32: + context.WriteVarUInt(prop.GetUInt32(obj)); + return; + case PropertyAccessorType.UInt64: + context.WriteVarULong(prop.GetUInt64(obj)); + return; + } + } + #endregion #region Specialized Array Writers diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs index 927b822..f58ef46 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs @@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions /// allowing the deserializer to match properties by name between different types. /// Default: false (no overhead) /// - public bool UseMetadata { get; init; } = true; + public bool UseMetadata { get; init; } = false; /// /// When true, checks for duplicate property name hashes during serialization (UseMetadata mode). diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs index b68b09a..1450eb4 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs @@ -31,6 +31,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase /// public Func DynamicGetter => _dynamicGetter; + /// + /// Pre-computed expected type code for markerless serialization (UseMetadata=false). + /// When set, the serializer skips writing the type marker byte and the deserializer + /// uses this value instead of reading from the stream. + /// null = marker must be written/read (bool, enum, string, object, collection, nullable value types). + /// Non-null = markerless (non-nullable value types: int, long, double, float, decimal, Guid, DateTime, byte, short, ushort, uint, ulong). + /// + public byte? ExpectedTypeCode { get; } + protected BinaryPropertyAccessorBase(PropertyInfo prop, Type declaringType) : base(prop, declaringType) { @@ -39,5 +48,28 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase // Cache string intern attribute (inherit: true to check base class properties) var attr = prop.GetCustomAttribute(inherit: true); IsStringInternProperty = attr?.Enabled; + + ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType); } + + /// + /// Maps AccessorType to the BinaryTypeCode that would normally be written as marker. + /// Returns null for types that always need a stream marker (bool, enum, string, object/nullable). + /// + private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch + { + PropertyAccessorType.Int32 => BinaryTypeCode.Int32, + PropertyAccessorType.Int64 => BinaryTypeCode.Int64, + PropertyAccessorType.Double => BinaryTypeCode.Float64, + PropertyAccessorType.Single => BinaryTypeCode.Float32, + PropertyAccessorType.Decimal => BinaryTypeCode.Decimal, + PropertyAccessorType.DateTime => BinaryTypeCode.DateTime, + PropertyAccessorType.Guid => BinaryTypeCode.Guid, + PropertyAccessorType.Byte => BinaryTypeCode.UInt8, + PropertyAccessorType.Int16 => BinaryTypeCode.Int16, + PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16, + PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32, + PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64, + _ => null // Bool, Enum, String, Object — always read marker from stream + }; } diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs index ca04990..8fbc474 100644 --- a/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs +++ b/AyCode.Core/Serializers/Binaries/BinaryPropertySetterBase.cs @@ -23,11 +23,20 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase /// public bool IsCollection { get; } + /// + /// Pre-computed expected type code for markerless deserialization (UseMetadata=false). + /// When set, the deserializer uses this value instead of reading a marker from the stream. + /// null = marker must be read from stream (bool, enum, string, object, collection, nullable value types). + /// Non-null = markerless (non-nullable value types). + /// + public byte? ExpectedTypeCode { get; } + protected BinaryPropertySetterBase(PropertyInfo prop, Type declaringType) : base(prop, declaringType) { IsCollection = IsCollectionTypeCheck(PropertyType); IsComplexType = IsComplex(PropertyType); + ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType); } public override void SetValue(object target, object? value) @@ -83,4 +92,25 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase if (ReferenceEquals(actualType, DateTimeOffsetType)) return false; return true; } + + /// + /// Maps AccessorType to the BinaryTypeCode that would normally be read as marker. + /// Returns null for types that always need a stream marker (bool, enum, string, object/nullable). + /// + private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch + { + PropertyAccessorType.Int32 => BinaryTypeCode.Int32, + PropertyAccessorType.Int64 => BinaryTypeCode.Int64, + PropertyAccessorType.Double => BinaryTypeCode.Float64, + PropertyAccessorType.Single => BinaryTypeCode.Float32, + PropertyAccessorType.Decimal => BinaryTypeCode.Decimal, + PropertyAccessorType.DateTime => BinaryTypeCode.DateTime, + PropertyAccessorType.Guid => BinaryTypeCode.Guid, + PropertyAccessorType.Byte => BinaryTypeCode.UInt8, + PropertyAccessorType.Int16 => BinaryTypeCode.Int16, + PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16, + PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32, + PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64, + _ => null // Bool, Enum, String, Object — always read marker from stream + }; }