Markerless serialization for value types (UseMetadata=false)
Introduced markerless serialization/deserialization for non-nullable value type properties when UseMetadata is false, eliminating type marker bytes for int, long, double, etc. Added ExpectedTypeCode to property accessors/setters to enable this optimization. Refactored property loops in serializer/deserializer for performance and clarity. Default UseMetadata is now false. Improves speed and reduces stream size for common value types while maintaining compatibility for complex types.
This commit is contained in:
parent
b38fd480d8
commit
97b7813633
|
|
@ -77,17 +77,57 @@ public static partial class AcBinaryDeserializer
|
|||
// Non-UseMetadata: properties.Length a target property-k száma (source == target)
|
||||
var propCount = cacheMap?.Length ?? properties.Length;
|
||||
|
||||
if (!context.HasMetadata)
|
||||
{
|
||||
// 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++)
|
||||
{
|
||||
var propInfo = properties[i];
|
||||
|
||||
if (propInfo.ExpectedTypeCode.HasValue)
|
||||
{
|
||||
ReadAndSetMarkerlessValue(ref context, target, propInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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 propInfo = cacheMap != null ? cacheMap[i] : properties[i];
|
||||
|
||||
PopulatePropertyWithMarker(ref context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
|
||||
/// </summary>
|
||||
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);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip marker - property has default/null value
|
||||
|
|
@ -101,7 +141,7 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
SetPropertyToDefault(target, propInfo);
|
||||
}
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// Null values - always set
|
||||
|
|
@ -109,7 +149,7 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
context.ReadByte(); // consume Null marker
|
||||
propInfo.SetValue(target, null);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle collections
|
||||
|
|
@ -130,7 +170,7 @@ public static partial class AcBinaryDeserializer
|
|||
// Normal populate: replace collection contents
|
||||
PopulateListOptimized(ref context, existingList, propInfo, nextDepth);
|
||||
}
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +187,7 @@ public static partial class AcBinaryDeserializer
|
|||
var nestedMeta = context.ContextClass.GetWrapper(propInfo.PropertyType).Metadata;
|
||||
CopyProperties(nestedValue, existingObj, nestedMeta);
|
||||
}
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +198,7 @@ public static partial class AcBinaryDeserializer
|
|||
// Use typed setters for primitives and strings to avoid ReadValue dispatch
|
||||
if (propInfo.AccessorType != PropertyAccessorType.Object &&
|
||||
TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
||||
continue;
|
||||
return;
|
||||
|
||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||
propInfo.SetValue(target, value);
|
||||
|
|
@ -167,7 +207,7 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
var targetType = target.GetType();
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetType.Name}'. " +
|
||||
$"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}. " +
|
||||
|
|
@ -180,6 +220,54 @@ public static partial class AcBinaryDeserializer
|
|||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -944,29 +944,48 @@ 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;
|
||||
|
||||
if (!context.UseMetadata)
|
||||
{
|
||||
// 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++)
|
||||
{
|
||||
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];
|
||||
|
||||
// Skip if filter says no - write skip marker
|
||||
if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Write property value OR skip marker (single operation, single getter call)
|
||||
WritePropertyOrSkip(value, prop, context, nextDepth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a property value is null or default without boxing for value types.
|
||||
|
|
@ -1283,6 +1302,55 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// allowing the deserializer to match properties by name between different types.
|
||||
/// Default: false (no overhead)
|
||||
/// </summary>
|
||||
public bool UseMetadata { get; init; } = true;
|
||||
public bool UseMetadata { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||
|
|
|
|||
|
|
@ -31,6 +31,15 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
/// </summary>
|
||||
public Func<object, object?> DynamicGetter => _dynamicGetter;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<AcStringInternAttribute>(inherit: true);
|
||||
IsStringInternProperty = attr?.Enabled;
|
||||
|
||||
ExpectedTypeCode = ComputeExpectedTypeCode(AccessorType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,20 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase
|
|||
/// </summary>
|
||||
public bool IsCollection { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue