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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
_position += length;
|
||||
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).
|
||||
/// No hashcode prefix - position-based footer handles reference tracking.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void PopulateObjectCore<TInput>(
|
||||
BinaryDeserializationContext<TInput> context,
|
||||
object target,
|
||||
|
|
@ -336,6 +337,12 @@ public static partial class AcBinaryDeserializer
|
|||
case PropertyAccessorType.UInt64:
|
||||
propInfo.SetUInt64(target, context.ReadVarULong());
|
||||
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++)
|
||||
{
|
||||
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 (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
|
||||
if (i < existingCount && typeCode == BinaryTypeCode.Object && elementMetadata != null)
|
||||
{
|
||||
var existingItem = existingList[i];
|
||||
if (existingItem != null)
|
||||
{
|
||||
context.ReadByte(); // consume Object marker
|
||||
PopulateObjectCore(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
|
||||
PopulateObjectPropertiesIndexed(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Marker already consumed → rewind so ReadValue can read it
|
||||
context._position--;
|
||||
value = ReadValue(context, elementType, nextDepth);
|
||||
}
|
||||
|
||||
|
|
@ -508,21 +516,22 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
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
|
||||
object? newItem;
|
||||
if (itemCode == BinaryTypeCode.Object)
|
||||
{
|
||||
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
||||
context.ReadByte(); // consume Object marker
|
||||
newItem = CreateInstance(elementType, elementMetadata);
|
||||
if (newItem == null) continue;
|
||||
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||
PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||
}
|
||||
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);
|
||||
if (newItem == null) continue;
|
||||
}
|
||||
|
|
@ -616,21 +625,22 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
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
|
||||
object? newItem;
|
||||
if (itemCode == BinaryTypeCode.Object)
|
||||
{
|
||||
// Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
|
||||
context.ReadByte(); // consume Object marker
|
||||
newItem = CreateInstance(elementType, elementMetadata);
|
||||
if (newItem == null) continue;
|
||||
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||
PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
|
||||
}
|
||||
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);
|
||||
if (newItem == null) continue;
|
||||
}
|
||||
|
|
@ -640,7 +650,7 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
// Track this ID as seen in source
|
||||
sourceIds?.Add(itemId);
|
||||
|
||||
|
||||
if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
|
||||
{
|
||||
// Copy properties to existing item (preserves reference)
|
||||
|
|
|
|||
|
|
@ -1536,6 +1536,12 @@ public static partial class AcBinarySerializer
|
|||
case PropertyAccessorType.UInt64:
|
||||
context.WriteVarULong(prop.GetUInt64(obj));
|
||||
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>
|
||||
/// 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>
|
||||
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
||||
{
|
||||
|
|
@ -110,6 +110,8 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
|
|||
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
||||
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
||||
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>
|
||||
/// 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>
|
||||
private static byte? ComputeExpectedTypeCode(PropertyAccessorType accessorType) => accessorType switch
|
||||
{
|
||||
|
|
@ -118,6 +118,8 @@ public abstract class BinaryPropertySetterBase : PropertySetterBase
|
|||
PropertyAccessorType.UInt16 => BinaryTypeCode.UInt16,
|
||||
PropertyAccessorType.UInt32 => BinaryTypeCode.UInt32,
|
||||
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