Optimize bool/enum serialization and ASCII string decoding
Improves binary serializer/deserializer performance by adding fast-path handling for boolean and enum properties, mapping their type codes directly for efficient read/write. Introduces a fast ASCII-only string decoding path for short strings, bypassing UTF8 overhead. Refactors array/list population to reduce redundant marker reads. Also applies aggressive inlining to core populate logic for further speedup.
This commit is contained in:
parent
e50dca93fa
commit
58cf9578c7
|
|
@ -319,6 +319,20 @@ public static partial class AcBinaryDeserializer
|
||||||
return ReadStringUtf8Cached(length);
|
return ReadStringUtf8Cached(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ASCII fast path: short strings (?128 bytes) with all ASCII bytes
|
||||||
|
// use string.Create + direct byte?char widening, avoiding UTF8Encoding overhead.
|
||||||
|
if (length <= 128 && System.Text.Ascii.IsValid(_buffer.AsSpan(_position, length)))
|
||||||
|
{
|
||||||
|
var pos = _position;
|
||||||
|
_position += length;
|
||||||
|
return string.Create(length, (Buffer: _buffer, Start: pos), static (chars, state) =>
|
||||||
|
{
|
||||||
|
var src = state.Buffer.AsSpan(state.Start, chars.Length);
|
||||||
|
for (int i = 0; i < chars.Length; i++)
|
||||||
|
chars[i] = (char)src[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var value = Utf8NoBom.GetString(_buffer, _position, length);
|
var value = Utf8NoBom.GetString(_buffer, _position, length);
|
||||||
_position += length;
|
_position += length;
|
||||||
return value;
|
return value;
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ public static partial class AcBinaryDeserializer
|
||||||
/// Wire format: All properties are written WITH type markers (including Id for IId types).
|
/// Wire format: All properties are written WITH type markers (including Id for IId types).
|
||||||
/// No hashcode prefix - position-based footer handles reference tracking.
|
/// No hashcode prefix - position-based footer handles reference tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void PopulateObjectCore<TInput>(
|
private static void PopulateObjectCore<TInput>(
|
||||||
BinaryDeserializationContext<TInput> context,
|
BinaryDeserializationContext<TInput> context,
|
||||||
object target,
|
object target,
|
||||||
|
|
@ -336,6 +337,12 @@ public static partial class AcBinaryDeserializer
|
||||||
case PropertyAccessorType.UInt64:
|
case PropertyAccessorType.UInt64:
|
||||||
propInfo.SetUInt64(target, context.ReadVarULong());
|
propInfo.SetUInt64(target, context.ReadVarULong());
|
||||||
return;
|
return;
|
||||||
|
case PropertyAccessorType.Boolean:
|
||||||
|
propInfo.SetBoolean(target, context.ReadByte() != 0);
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Enum:
|
||||||
|
propInfo.SetEnumAsInt32(target, context.ReadVarInt());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,29 +417,30 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var peekCode = context.PeekByte();
|
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
|
||||||
|
var typeCode = context.ReadByte();
|
||||||
|
|
||||||
// If we have an existing item at this index and the incoming is an object, reuse it
|
// If we have an existing item at this index and the incoming is an object, reuse it
|
||||||
if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
|
if (i < existingCount && typeCode == BinaryTypeCode.Object && elementMetadata != null)
|
||||||
{
|
{
|
||||||
var existingItem = existingList[i];
|
var existingItem = existingList[i];
|
||||||
if (existingItem != null)
|
if (existingItem != null)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Object marker
|
PopulateObjectPropertiesIndexed(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
|
||||||
PopulateObjectCore(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
|
// Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
|
||||||
object? value;
|
object? value;
|
||||||
if (peekCode == BinaryTypeCode.Object && elementMetadata != null)
|
if (typeCode == BinaryTypeCode.Object && elementMetadata != null)
|
||||||
{
|
{
|
||||||
context.ReadByte(); // consume Object marker
|
|
||||||
value = ReadObjectCoreWithWrapper(context, wrapper, nextDepth, cacheIndex: -1);
|
value = ReadObjectCoreWithWrapper(context, wrapper, nextDepth, cacheIndex: -1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Marker already consumed → rewind so ReadValue can read it
|
||||||
|
context._position--;
|
||||||
value = ReadValue(context, elementType, nextDepth);
|
value = ReadValue(context, elementType, nextDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,21 +516,22 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
for (int i = 0; i < arrayCount; i++)
|
for (int i = 0; i < arrayCount; i++)
|
||||||
{
|
{
|
||||||
var itemCode = context.PeekByte();
|
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
|
||||||
|
var itemCode = context.ReadByte();
|
||||||
|
|
||||||
// Read or create the new item
|
// Read or create the new item
|
||||||
object? newItem;
|
object? newItem;
|
||||||
if (itemCode == BinaryTypeCode.Object)
|
if (itemCode == BinaryTypeCode.Object)
|
||||||
{
|
{
|
||||||
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
||||||
context.ReadByte(); // consume Object marker
|
|
||||||
newItem = CreateInstance(elementType, elementMetadata);
|
newItem = CreateInstance(elementType, elementMetadata);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback for non-Object markers (null, ObjectRef, etc.)
|
// Marker already consumed → rewind so ReadValue can read it
|
||||||
|
context._position--;
|
||||||
newItem = ReadValue(context, elementType, nextDepth);
|
newItem = ReadValue(context, elementType, nextDepth);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
}
|
}
|
||||||
|
|
@ -616,21 +625,22 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
for (int i = 0; i < arrayCount; i++)
|
for (int i = 0; i < arrayCount; i++)
|
||||||
{
|
{
|
||||||
var itemCode = context.PeekByte();
|
// Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
|
||||||
|
var itemCode = context.ReadByte();
|
||||||
|
|
||||||
// Read or create the new item
|
// Read or create the new item
|
||||||
object? newItem;
|
object? newItem;
|
||||||
if (itemCode == BinaryTypeCode.Object)
|
if (itemCode == BinaryTypeCode.Object)
|
||||||
{
|
{
|
||||||
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
||||||
context.ReadByte(); // consume Object marker
|
|
||||||
newItem = CreateInstance(elementType, elementMetadata);
|
newItem = CreateInstance(elementType, elementMetadata);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback for non-Object markers (null, ObjectRef, etc.)
|
// Marker already consumed → rewind so ReadValue can read it
|
||||||
|
context._position--;
|
||||||
newItem = ReadValue(context, elementType, nextDepth);
|
newItem = ReadValue(context, elementType, nextDepth);
|
||||||
if (newItem == null) continue;
|
if (newItem == null) continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1536,6 +1536,12 @@ public static partial class AcBinarySerializer
|
||||||
case PropertyAccessorType.UInt64:
|
case PropertyAccessorType.UInt64:
|
||||||
context.WriteVarULong(prop.GetUInt64(obj));
|
context.WriteVarULong(prop.GetUInt64(obj));
|
||||||
return;
|
return;
|
||||||
|
case PropertyAccessorType.Boolean:
|
||||||
|
context.WriteByte(prop.GetBoolean(obj) ? (byte)1 : (byte)0);
|
||||||
|
return;
|
||||||
|
case PropertyAccessorType.Enum:
|
||||||
|
context.WriteVarInt(prop.GetEnumAsInt32(obj));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps AccessorType to the BinaryTypeCode that would normally be written as marker.
|
/// 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).
|
/// Returns null for types that always need a stream marker (string, object/nullable).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
||||||
{
|
{
|
||||||
|
|
@ -110,6 +110,8 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
||||||
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
||||||
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
||||||
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
|
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
|
||||||
_ => null // Bool, Enum, String, Object — always read marker from stream
|
PropertyAccessorType.Boolean => BinaryTypeCode.True,
|
||||||
|
PropertyAccessorType.Enum => BinaryTypeCode.Enum,
|
||||||
|
_ => null // String, Object — always write marker to stream
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps AccessorType to the BinaryTypeCode that would normally be read as marker.
|
/// 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).
|
/// Returns null for types that always need a stream marker (string, object/nullable).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
||||||
{
|
{
|
||||||
|
|
@ -118,6 +118,8 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase
|
||||||
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
||||||
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
||||||
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
|
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
|
||||||
_ => null // Bool, Enum, String, Object — always read marker from stream
|
PropertyAccessorType.Boolean => BinaryTypeCode.True,
|
||||||
|
PropertyAccessorType.Enum => BinaryTypeCode.Enum,
|
||||||
|
_ => null // String, Object — always read marker from stream
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue