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:
Loretta 2026-02-15 10:37:12 +01:00
parent e50dca93fa
commit 58cf9578c7
5 changed files with 53 additions and 19 deletions

View File

@ -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;

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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
};
}

View File

@ -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
};
}