Optimize binary serialization with PropertySkip marker
Refactored object serialization to use fixed property order and a new PropertySkip marker for default/null values, removing property count and index overhead. Updated deserialization logic to handle skip markers and set defaults efficiently. Added WritePropertyOrSkip and SetPropertyToDefault methods for single-pass, boxing-free property handling. SkipObject now throws for new format. Updated BinaryTypeCode, .gitignore, and settings.local.json.
This commit is contained in:
parent
4b9e1490ef
commit
65a1d25586
|
|
@ -10,7 +10,21 @@
|
|||
"Bash(findstr:*)",
|
||||
"Bash(cmd /c \"cd /d H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark && dir /B /O-D *.log\")",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git checkout:*)"
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(dir /B \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\AyCode.Benchmark\\*.cs\")",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(cmd /c \"dir /B /O-D *.log\")",
|
||||
"Bash(printf:*)",
|
||||
"Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log\")",
|
||||
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log\")",
|
||||
"Bash(powershell:*)",
|
||||
"Bash(timeout /t 60 /nobreak)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(timeout /t 3 /nobreak)",
|
||||
"Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")",
|
||||
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")",
|
||||
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -3\")",
|
||||
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ bin/
|
|||
/.vs/*
|
||||
/.vs/**
|
||||
|
||||
/.claude/*
|
||||
/.claude/**
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
|
|
|
|||
|
|
@ -26,30 +26,41 @@ public static partial class AcBinaryDeserializer
|
|||
PopulateObject(ref context, target, metadata, depth);
|
||||
}
|
||||
|
||||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth)
|
||||
private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite = false)
|
||||
{
|
||||
var propertyCount = (int)context.ReadVarUInt();
|
||||
// NEW FORMAT: Fixed property order with SKIP markers
|
||||
// - No property count (iterate all properties in metadata order)
|
||||
// - No property indices (use sequential PropertyIndex = i)
|
||||
// - PropertySkip marker indicates default/null value
|
||||
//
|
||||
// skipDefaultWrite:
|
||||
// true = Deserialize mode - object just created, properties already at default, skip writing
|
||||
// false = Populate mode - existing object, must overwrite properties with default values
|
||||
|
||||
var properties = metadata.PropertiesArray;
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
{
|
||||
var propertyIndexStartPosition = context.Position;
|
||||
|
||||
// Read property index directly - no string lookup needed!
|
||||
var propIndex = (int)context.ReadVarUInt();
|
||||
|
||||
// O(1) array lookup instead of dictionary lookup
|
||||
var propInfo = metadata.GetPropertyByIndex(propIndex);
|
||||
|
||||
if (propInfo == null)
|
||||
var propInfo = properties[i];
|
||||
|
||||
var peekCode = context.PeekByte();
|
||||
|
||||
// OPTIMIZATION: Skip marker - property has default/null value
|
||||
if (peekCode == BinaryTypeCode.PropertySkip)
|
||||
{
|
||||
// Skip unknown property (schema evolution - new property on sender side)
|
||||
SkipValue(ref context);
|
||||
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);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var peekCode = context.PeekByte();
|
||||
|
||||
// OPTIMIZATION: Skip null values early - no GetValue call needed!
|
||||
if (peekCode == BinaryTypeCode.Null)
|
||||
{
|
||||
|
|
@ -91,7 +102,7 @@ public static partial class AcBinaryDeserializer
|
|||
// OPTIMIZATION: Use typed setters for primitives to avoid boxing
|
||||
if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode))
|
||||
continue;
|
||||
|
||||
|
||||
var value = ReadValue(ref context, propInfo.PropertyType, nextDepth);
|
||||
propInfo.SetValue(target, value);
|
||||
}
|
||||
|
|
@ -100,9 +111,9 @@ public static partial class AcBinaryDeserializer
|
|||
// Only get type info when needed for error message (cold path)
|
||||
var targetType = target.GetType();
|
||||
var targetTypeName = targetType.Name;
|
||||
|
||||
|
||||
throw new AcBinaryDeserializationException(
|
||||
$"Type mismatch for property '{propInfo.Name}' (index {i}/{propertyCount}, propIndex={propIndex}) on '{targetTypeName}'. " +
|
||||
$"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetTypeName}'. " +
|
||||
$"Expected type: '{propInfo.PropertyType.FullName}'. " +
|
||||
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
|
||||
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
|
||||
|
|
@ -137,7 +148,7 @@ public static partial class AcBinaryDeserializer
|
|||
PopulateObject(ref context, target, metadata, depth);
|
||||
}
|
||||
|
||||
private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
|
||||
private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth, bool skipDefaultWrite = false)
|
||||
{
|
||||
var metadata = GetTypeMetadata(targetType);
|
||||
|
||||
|
|
@ -151,23 +162,31 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
var propertyCount = (int)context.ReadVarUInt();
|
||||
// NEW FORMAT: Fixed property order with SKIP markers
|
||||
var properties = metadata.PropertiesArray;
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
for (int i = 0; i < propertyCount; i++)
|
||||
for (int i = 0; i < properties.Length; i++)
|
||||
{
|
||||
// Read property index directly - O(1) array lookup
|
||||
var propIndex = (int)context.ReadVarUInt();
|
||||
var propInfo = metadata.GetPropertyByIndex(propIndex);
|
||||
|
||||
if (propInfo == null)
|
||||
{
|
||||
SkipValue(ref context);
|
||||
continue;
|
||||
}
|
||||
var propInfo = properties[i];
|
||||
|
||||
var peekCode = context.PeekByte();
|
||||
|
||||
// OPTIMIZATION: Skip marker - property has default/null value
|
||||
if (peekCode == BinaryTypeCode.PropertySkip)
|
||||
{
|
||||
context.ReadByte(); // consume Skip marker
|
||||
|
||||
// Populate mode: overwrite with default (same as PopulateObject)
|
||||
// Deserialize mode: skip write (new object already has defaults)
|
||||
if (!skipDefaultWrite)
|
||||
{
|
||||
SetPropertyToDefault(target, propInfo);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle IId collection merge
|
||||
if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array)
|
||||
{
|
||||
|
|
@ -315,7 +334,8 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
PopulateObject(ref context, existingItem, elementMetadata, nextDepth);
|
||||
// Populate mode: existing item, must overwrite with defaults
|
||||
PopulateObject(ref context, existingItem, elementMetadata, nextDepth, skipDefaultWrite: false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -415,7 +435,8 @@ public static partial class AcBinaryDeserializer
|
|||
context.RegisterObject(refId, newItem);
|
||||
}
|
||||
|
||||
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
|
||||
// Deserialize mode: new item just created, skip writing defaults
|
||||
PopulateObject(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true);
|
||||
|
||||
var itemId = idGetter(newItem);
|
||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||
|
|
@ -527,7 +548,8 @@ public static partial class AcBinaryDeserializer
|
|||
context.RegisterObject(refId, newItem);
|
||||
}
|
||||
|
||||
PopulateObject(ref context, newItem, elementMetadata, nextDepth);
|
||||
// Deserialize mode: new item just created, skip writing defaults
|
||||
PopulateObject(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true);
|
||||
|
||||
var itemId = idGetter(newItem);
|
||||
if (itemId != null && !IsDefaultValue(itemId, idType))
|
||||
|
|
@ -584,6 +606,59 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property to its default value using typed setters to avoid boxing.
|
||||
/// For collections and complex objects, clears the collection instead of setting to null.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void SetPropertyToDefault(object target, BinaryPropertySetterInfo propInfo)
|
||||
{
|
||||
switch (propInfo.SetterType)
|
||||
{
|
||||
case PropertyAccessorType.Int32:
|
||||
propInfo.SetInt32(target, 0);
|
||||
return;
|
||||
case PropertyAccessorType.Int64:
|
||||
propInfo.SetInt64(target, 0L);
|
||||
return;
|
||||
case PropertyAccessorType.Boolean:
|
||||
propInfo.SetBoolean(target, false);
|
||||
return;
|
||||
case PropertyAccessorType.Double:
|
||||
propInfo.SetDouble(target, 0.0);
|
||||
return;
|
||||
case PropertyAccessorType.Single:
|
||||
propInfo.SetSingle(target, 0f);
|
||||
return;
|
||||
case PropertyAccessorType.Decimal:
|
||||
propInfo.SetDecimal(target, 0m);
|
||||
return;
|
||||
case PropertyAccessorType.DateTime:
|
||||
propInfo.SetDateTime(target, default);
|
||||
return;
|
||||
case PropertyAccessorType.Guid:
|
||||
propInfo.SetGuid(target, default);
|
||||
return;
|
||||
case PropertyAccessorType.Object:
|
||||
default:
|
||||
// For collections: clear instead of setting to null
|
||||
if (propInfo.IsCollection)
|
||||
{
|
||||
var collection = propInfo.GetValue(target);
|
||||
if (collection is IList list)
|
||||
{
|
||||
list.Clear();
|
||||
}
|
||||
// If not IList or null, leave it as-is (readonly collection or null already)
|
||||
return;
|
||||
}
|
||||
|
||||
// For other objects: set to null (strings, nullable types, etc.)
|
||||
propInfo.SetValue(target, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a type is a complex type (not primitive, string, or simple value type).
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -927,7 +927,8 @@ public static partial class AcBinaryDeserializer
|
|||
context.RegisterObject(refId, instance);
|
||||
}
|
||||
|
||||
PopulateObject(ref context, instance, metadata, depth);
|
||||
// Deserialize mode: object just created, skip writing defaults (already at default)
|
||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||
|
||||
// ChainMode: Check if we already have an object with this Id in the tracker
|
||||
// Use cached IdType from metadata instead of id.GetType() to avoid reflection overhead
|
||||
|
|
@ -1316,15 +1317,12 @@ public static partial class AcBinaryDeserializer
|
|||
context.ReadVarInt();
|
||||
}
|
||||
|
||||
var propCount = (int)context.ReadVarUInt();
|
||||
for (int i = 0; i < propCount; i++)
|
||||
{
|
||||
// Skip property index (VarUInt)
|
||||
context.ReadVarUInt();
|
||||
|
||||
// Skip value
|
||||
SkipValue(ref context);
|
||||
}
|
||||
// NEW FORMAT: Can't skip without knowing property count!
|
||||
// Need to read type metadata to know how many properties to skip
|
||||
// For now, throw exception - SkipObject not supported with new format
|
||||
throw new NotSupportedException(
|
||||
"SkipObject is not supported with SKIP marker format. " +
|
||||
"Unable to determine property count without type metadata.");
|
||||
}
|
||||
|
||||
private static void SkipArray(ref BinaryDeserializationContext context)
|
||||
|
|
|
|||
|
|
@ -712,37 +712,26 @@ public static partial class AcBinarySerializer
|
|||
var properties = metadata.Properties;
|
||||
var propCount = properties.Length;
|
||||
|
||||
// Reserve space for property count (will patch later)
|
||||
var countPosition = context.Position;
|
||||
context.WriteVarUInt(0); // Placeholder - will be patched
|
||||
|
||||
var writtenCount = 0;
|
||||
// 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
|
||||
|
||||
// Single pass: check and write in one iteration
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var prop = properties[i];
|
||||
|
||||
// Skip if filter says no
|
||||
// Skip if filter says no - write skip marker
|
||||
if (context.PropertyFilter != null && !context.ShouldSerializeProperty(value, prop))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip default/null values
|
||||
if (IsPropertyDefaultOrNull(value, prop))
|
||||
continue;
|
||||
|
||||
// Write property index directly - no dictionary lookup needed!
|
||||
// PropertyIndex is deterministic (based on alphabetical ordering)
|
||||
// and consistent across all platforms (WASM, iOS, Android, Windows)
|
||||
context.WriteVarUInt((uint)prop.PropertyIndex);
|
||||
|
||||
// Write property value
|
||||
WritePropertyValue(value, prop, context, nextDepth);
|
||||
writtenCount++;
|
||||
// Write property value OR skip marker (single operation, single getter call)
|
||||
WritePropertyOrSkip(value, prop, context, nextDepth);
|
||||
}
|
||||
|
||||
// Patch the property count
|
||||
context.PatchVarUInt(countPosition, (uint)writtenCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -861,6 +850,171 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a property value OR a skip marker if the value is default/null.
|
||||
/// Single-pass optimization: checks default + writes value in one operation.
|
||||
/// Avoids double getter calls.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WritePropertyOrSkip(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth)
|
||||
{
|
||||
switch (prop.AccessorType)
|
||||
{
|
||||
case PropertyAccessorType.Int32:
|
||||
{
|
||||
int value = prop.GetInt32(obj);
|
||||
if (value == 0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteInt32(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Int64:
|
||||
{
|
||||
long value = prop.GetInt64(obj);
|
||||
if (value == 0L)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteInt64(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Boolean:
|
||||
{
|
||||
bool value = prop.GetBoolean(obj);
|
||||
if (!value)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
context.WriteByte(BinaryTypeCode.True);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Double:
|
||||
{
|
||||
double value = prop.GetDouble(obj);
|
||||
if (value == 0.0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteFloat64Unsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Single:
|
||||
{
|
||||
float value = prop.GetSingle(obj);
|
||||
if (value == 0f)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteFloat32Unsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Decimal:
|
||||
{
|
||||
decimal value = prop.GetDecimal(obj);
|
||||
if (value == 0m)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteDecimalUnsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.DateTime:
|
||||
{
|
||||
DateTime value = prop.GetDateTime(obj);
|
||||
// DateTime always written (no default skip)
|
||||
WriteDateTimeUnsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Byte:
|
||||
{
|
||||
byte value = prop.GetByte(obj);
|
||||
if (value == 0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.UInt8);
|
||||
context.WriteByte(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Int16:
|
||||
{
|
||||
short value = prop.GetInt16(obj);
|
||||
if (value == 0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteInt16Unsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.UInt16:
|
||||
{
|
||||
ushort value = prop.GetUInt16(obj);
|
||||
if (value == 0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteUInt16Unsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.UInt32:
|
||||
{
|
||||
uint value = prop.GetUInt32(obj);
|
||||
if (value == 0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteUInt32(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.UInt64:
|
||||
{
|
||||
ulong value = prop.GetUInt64(obj);
|
||||
if (value == 0)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteUInt64(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Guid:
|
||||
{
|
||||
Guid value = prop.GetGuid(obj);
|
||||
if (value == Guid.Empty)
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
else
|
||||
WriteGuidUnsafe(value, context);
|
||||
return;
|
||||
}
|
||||
case PropertyAccessorType.Enum:
|
||||
{
|
||||
int enumValue = prop.GetEnumAsInt32(obj);
|
||||
if (enumValue == 0)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
}
|
||||
else if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Enum);
|
||||
context.WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Enum);
|
||||
context.WriteByte(BinaryTypeCode.Int32);
|
||||
context.WriteVarInt(enumValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// Object type - use regular getter
|
||||
var value = prop.GetValue(obj);
|
||||
if (value == null || (prop.PropertyTypeCode == TypeCode.String && string.IsNullOrEmpty((string)value)))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.PropertySkip);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteValue(value, prop.PropertyType, context, depth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Specialized Array Writers
|
||||
|
|
|
|||
|
|
@ -213,6 +213,9 @@ internal static class BinaryTypeCode
|
|||
// Compact integer variants (for VarInt optimization)
|
||||
public const byte Int32Tiny = 192; // -16 to 63 stored in single byte (value = code - 192 - 16)
|
||||
public const byte Int32TinyMax = 255; // Upper bound for tiny int (192 + 64 - 1 = 255)
|
||||
|
||||
// Property skip marker (for single-pass serialization optimization)
|
||||
public const byte PropertySkip = 253; // Marks a property with default/null value (skipped during serialization)
|
||||
|
||||
/// <summary>
|
||||
/// Check if type code represents a reference (string or object).
|
||||
|
|
|
|||
Loading…
Reference in New Issue