diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c1e4355..8866d3a 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -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\")"
]
}
}
diff --git a/.gitignore b/.gitignore
index c70e59d..172eb08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,9 @@ bin/
/.vs/*
/.vs/**
+/.claude/*
+/.claude/**
+
# User-specific files
*.rsuser
*.suo
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
index a2e0c14..bdb7a1b 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs
@@ -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
}
}
+ ///
+ /// 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.
+ ///
+ [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;
+ }
+ }
+
///
/// Determines if a type is a complex type (not primitive, string, or simple value type).
///
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index f92d554..46fc890 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -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)
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index a0aa98f..b23fe23 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -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);
}
///
@@ -861,6 +850,171 @@ public static partial class AcBinarySerializer
}
}
+ ///
+ /// 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.
+ ///
+ [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
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
index 2b5f664..c2fa725 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializerOptions.cs
@@ -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)
///
/// Check if type code represents a reference (string or object).