Optimize AcBinarySerializer: typed accessors & array writers
- Add typed property accessors to avoid boxing and speed up value type serialization - Implement bulk array writers for primitives (int, long, double, etc.) for efficient, zero-copy serialization - Add zero-copy IBufferWriter serialization and size estimation methods - Refactor array/dictionary serialization for fast paths and memory efficiency - Improve context pool memory management and reduce initial dictionary/set capacities - Fix benchmark to avoid state accumulation between runs - Downgrade MessagePack dependency for compatibility
This commit is contained in:
parent
f69b14c195
commit
056aae97a5
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="MessagePack" Version="2.6.95-alpha" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
|
|
|||
|
|
@ -84,12 +84,80 @@ public static class AcBinarySerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize to existing buffer writer (for streaming scenarios).
|
||||
/// Serialize object to an IBufferWriter for zero-copy scenarios.
|
||||
/// This avoids the final ToArray() allocation by writing directly to the caller's buffer.
|
||||
/// </summary>
|
||||
public static void Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
||||
{
|
||||
var bytes = Serialize(value, options);
|
||||
writer.Write(bytes);
|
||||
if (value == null)
|
||||
{
|
||||
var span = writer.GetSpan(1);
|
||||
span[0] = BinaryTypeCode.Null;
|
||||
writer.Advance(1);
|
||||
return;
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
var context = BinarySerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
context.WriteHeaderPlaceholder();
|
||||
|
||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
ScanReferences(value, context, 0);
|
||||
}
|
||||
|
||||
if (options.UseMetadata && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
CollectPropertyNames(value, context, 0);
|
||||
}
|
||||
|
||||
context.WriteMetadata();
|
||||
WriteValue(value, type, context, 0);
|
||||
|
||||
// Write directly to the IBufferWriter instead of creating a new array
|
||||
context.WriteTo(writer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BinarySerializationContextPool.Return(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the serialized size without allocating the final array.
|
||||
/// Useful for pre-allocating buffers.
|
||||
/// </summary>
|
||||
public static int GetSerializedSize<T>(T value, AcBinarySerializerOptions options)
|
||||
{
|
||||
if (value == null) return 1;
|
||||
|
||||
var type = value.GetType();
|
||||
var context = BinarySerializationContextPool.Get(options);
|
||||
try
|
||||
{
|
||||
context.WriteHeaderPlaceholder();
|
||||
|
||||
if (options.UseReferenceHandling && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
ScanReferences(value, context, 0);
|
||||
}
|
||||
|
||||
if (options.UseMetadata && !IsPrimitiveOrStringFast(type))
|
||||
{
|
||||
CollectPropertyNames(value, context, 0);
|
||||
}
|
||||
|
||||
context.WriteMetadata();
|
||||
WriteValue(value, type, context, 0);
|
||||
|
||||
return context.Position;
|
||||
}
|
||||
finally
|
||||
{
|
||||
BinarySerializationContextPool.Return(context);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -460,7 +528,8 @@ public static class AcBinarySerializer
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteEnum(object value, BinarySerializationContext context)
|
||||
private static void WriteEnum(object value, BinarySerializationContext context
|
||||
)
|
||||
{
|
||||
var intValue = Convert.ToInt32(value);
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
|
|
@ -534,29 +603,28 @@ public static class AcBinarySerializer
|
|||
|
||||
var metadata = GetTypeMetadata(type);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
// Pre-count non-null, non-default properties
|
||||
var writtenCount = 0;
|
||||
var properties = metadata.Properties;
|
||||
var propCount = properties.Length;
|
||||
|
||||
// Single-pass: count and collect non-null, non-default properties
|
||||
// Use stackalloc for small property counts to avoid allocation
|
||||
Span<int> validIndices = propCount <= 32 ? stackalloc int[propCount] : new int[propCount];
|
||||
var writtenCount = 0;
|
||||
|
||||
for (var i = 0; i < propCount; i++)
|
||||
{
|
||||
var prop = properties[i];
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue == null) continue;
|
||||
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
|
||||
writtenCount++;
|
||||
if (IsPropertyDefaultOrNull(value, prop))
|
||||
continue;
|
||||
validIndices[writtenCount++] = i;
|
||||
}
|
||||
|
||||
context.WriteVarUInt((uint)writtenCount);
|
||||
|
||||
for (var i = 0; i < propCount; i++)
|
||||
// Write only the valid properties
|
||||
for (var j = 0; j < writtenCount; j++)
|
||||
{
|
||||
var prop = properties[i];
|
||||
var propValue = prop.GetValue(value);
|
||||
if (propValue == null) continue;
|
||||
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
|
||||
var prop = properties[validIndices[j]];
|
||||
|
||||
// Write property index or name
|
||||
if (context.UseMetadata)
|
||||
|
|
@ -569,167 +637,124 @@ public static class AcBinarySerializer
|
|||
WriteString(prop.Name, context);
|
||||
}
|
||||
|
||||
WriteValue(propValue, prop.PropertyType, context, nextDepth);
|
||||
// Use typed writers to avoid boxing
|
||||
WritePropertyValue(value, prop, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized array writer with specialized paths for primitive arrays.
|
||||
/// </summary>
|
||||
private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Array);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
// Optimized path for primitive arrays using MemoryMarshal
|
||||
var elementType = GetCollectionElementType(type);
|
||||
if (elementType != null && type.IsArray)
|
||||
{
|
||||
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
||||
return;
|
||||
}
|
||||
|
||||
// For IList, we can write the count directly
|
||||
if (enumerable is IList list)
|
||||
{
|
||||
var count = list.Count;
|
||||
context.WriteVarUInt((uint)count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = list[i];
|
||||
var itemType = item?.GetType() ?? typeof(object);
|
||||
WriteValue(item, itemType, context, nextDepth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For other IEnumerable, collect first
|
||||
var items = new List<object?>();
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
context.WriteVarUInt((uint)items.Count);
|
||||
foreach (var item in items)
|
||||
{
|
||||
var itemType = item?.GetType() ?? typeof(object);
|
||||
WriteValue(item, itemType, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specialized array writer for primitive arrays using bulk memory operations.
|
||||
/// Checks if a property value is null or default without boxing for value types.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context)
|
||||
private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop)
|
||||
{
|
||||
// Int32 array - very common case
|
||||
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
|
||||
switch (prop.AccessorType)
|
||||
{
|
||||
context.WriteVarUInt((uint)intArray.Length);
|
||||
for (var i = 0; i < intArray.Length; i++)
|
||||
{
|
||||
WriteInt32(intArray[i], context);
|
||||
}
|
||||
return true;
|
||||
case PropertyAccessorType.Int32:
|
||||
return prop.GetInt32(obj) == 0;
|
||||
case PropertyAccessorType.Int64:
|
||||
return prop.GetInt64(obj) == 0L;
|
||||
case PropertyAccessorType.Boolean:
|
||||
return !prop.GetBoolean(obj);
|
||||
case PropertyAccessorType.Double:
|
||||
return prop.GetDouble(obj) == 0.0;
|
||||
case PropertyAccessorType.Single:
|
||||
return prop.GetSingle(obj) == 0f;
|
||||
case PropertyAccessorType.Decimal:
|
||||
return prop.GetDecimal(obj) == 0m;
|
||||
case PropertyAccessorType.Byte:
|
||||
return prop.GetByte(obj) == 0;
|
||||
case PropertyAccessorType.Int16:
|
||||
return prop.GetInt16(obj) == 0;
|
||||
case PropertyAccessorType.UInt16:
|
||||
return prop.GetUInt16(obj) == 0;
|
||||
case PropertyAccessorType.UInt32:
|
||||
return prop.GetUInt32(obj) == 0;
|
||||
case PropertyAccessorType.UInt64:
|
||||
return prop.GetUInt64(obj) == 0;
|
||||
case PropertyAccessorType.Guid:
|
||||
return prop.GetGuid(obj) == Guid.Empty;
|
||||
case PropertyAccessorType.Enum:
|
||||
return prop.GetEnumAsInt32(obj) == 0;
|
||||
case PropertyAccessorType.DateTime:
|
||||
// DateTime default is not typically skipped
|
||||
return false;
|
||||
default:
|
||||
// Object type - use regular getter
|
||||
var value = prop.GetValue(obj);
|
||||
if (value == null) return true;
|
||||
if (prop.TypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Double array
|
||||
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)doubleArray.Length);
|
||||
for (var i = 0; i < doubleArray.Length; i++)
|
||||
{
|
||||
WriteFloat64Unsafe(doubleArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Long array
|
||||
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)longArray.Length);
|
||||
for (var i = 0; i < longArray.Length; i++)
|
||||
{
|
||||
WriteInt64(longArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Float array
|
||||
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)floatArray.Length);
|
||||
for (var i = 0; i < floatArray.Length; i++)
|
||||
{
|
||||
WriteFloat32Unsafe(floatArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bool array
|
||||
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)boolArray.Length);
|
||||
for (var i = 0; i < boolArray.Length; i++)
|
||||
{
|
||||
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Guid array
|
||||
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)guidArray.Length);
|
||||
for (var i = 0; i < guidArray.Length; i++)
|
||||
{
|
||||
WriteGuidUnsafe(guidArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decimal array
|
||||
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)decimalArray.Length);
|
||||
for (var i = 0; i < decimalArray.Length; i++)
|
||||
{
|
||||
WriteDecimalUnsafe(decimalArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// DateTime array
|
||||
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)dateTimeArray.Length);
|
||||
for (var i = 0; i < dateTimeArray.Length; i++)
|
||||
{
|
||||
WriteDateTimeUnsafe(dateTimeArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth)
|
||||
/// <summary>
|
||||
/// Writes a property value using typed getters to avoid boxing.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WritePropertyValue(object obj, BinaryPropertyAccessor prop, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Dictionary);
|
||||
context.WriteVarUInt((uint)dictionary.Count);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
switch (prop.AccessorType)
|
||||
{
|
||||
// Write key
|
||||
var keyType = entry.Key?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Key, keyType, context, nextDepth);
|
||||
|
||||
// Write value
|
||||
var valueType = entry.Value?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Value, valueType, context, nextDepth);
|
||||
case PropertyAccessorType.Int32:
|
||||
WriteInt32(prop.GetInt32(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Int64:
|
||||
WriteInt64(prop.GetInt64(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Boolean:
|
||||
context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||
return;
|
||||
case PropertyAccessorType.Double:
|
||||
WriteFloat64Unsafe(prop.GetDouble(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Single:
|
||||
WriteFloat32Unsafe(prop.GetSingle(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Decimal:
|
||||
WriteDecimalUnsafe(prop.GetDecimal(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.DateTime:
|
||||
WriteDateTimeUnsafe(prop.GetDateTime(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Byte:
|
||||
context.WriteByte(BinaryTypeCode.UInt8);
|
||||
context.WriteByte(prop.GetByte(obj));
|
||||
return;
|
||||
case PropertyAccessorType.Int16:
|
||||
WriteInt16Unsafe(prop.GetInt16(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.UInt16:
|
||||
WriteUInt16Unsafe(prop.GetUInt16(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.UInt32:
|
||||
WriteUInt32(prop.GetUInt32(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.UInt64:
|
||||
WriteUInt64(prop.GetUInt64(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Guid:
|
||||
WriteGuidUnsafe(prop.GetGuid(obj), context);
|
||||
return;
|
||||
case PropertyAccessorType.Enum:
|
||||
var enumValue = prop.GetEnumAsInt32(obj);
|
||||
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:
|
||||
// Fallback to object getter for reference types
|
||||
var value = prop.GetValue(obj);
|
||||
WriteValue(value, prop.PropertyType, context, depth);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -839,13 +864,22 @@ public static class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized property accessor with typed getters to avoid boxing for common value types.
|
||||
/// </summary>
|
||||
internal sealed class BinaryPropertyAccessor
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly byte[] NameUtf8;
|
||||
public readonly Type PropertyType;
|
||||
public readonly TypeCode TypeCode;
|
||||
private readonly Func<object, object?> _getter;
|
||||
|
||||
// Generic getter (used for reference types and fallback)
|
||||
private readonly Func<object, object?> _objectGetter;
|
||||
|
||||
// Typed getters to avoid boxing - null if not applicable
|
||||
private readonly Delegate? _typedGetter;
|
||||
private readonly PropertyAccessorType _accessorType;
|
||||
|
||||
public BinaryPropertyAccessor(PropertyInfo prop)
|
||||
{
|
||||
|
|
@ -853,10 +887,80 @@ public static class AcBinarySerializer
|
|||
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
|
||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||
TypeCode = Type.GetTypeCode(PropertyType);
|
||||
_getter = CreateCompiledGetter(prop.DeclaringType!, prop);
|
||||
|
||||
var declaringType = prop.DeclaringType!;
|
||||
|
||||
// Create typed getter for value types to avoid boxing
|
||||
(_typedGetter, _accessorType) = CreateTypedGetter(declaringType, prop);
|
||||
|
||||
// Always create object getter as fallback
|
||||
_objectGetter = CreateObjectGetter(declaringType, prop);
|
||||
}
|
||||
|
||||
private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
|
||||
private static (Delegate?, PropertyAccessorType) CreateTypedGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var propType = prop.PropertyType;
|
||||
var underlyingType = Nullable.GetUnderlyingType(propType);
|
||||
var isNullable = underlyingType != null;
|
||||
var actualType = underlyingType ?? propType;
|
||||
|
||||
// For nullable types, we need special handling
|
||||
if (isNullable)
|
||||
{
|
||||
return (null, PropertyAccessorType.Object);
|
||||
}
|
||||
|
||||
// Check enum FIRST before TypeCode (enums have TypeCode.Int32 etc. based on underlying type)
|
||||
if (actualType.IsEnum)
|
||||
{
|
||||
return (CreateEnumGetterDelegate(declaringType, prop), PropertyAccessorType.Enum);
|
||||
}
|
||||
|
||||
// Check for Guid (no TypeCode)
|
||||
if (ReferenceEquals(actualType, GuidType))
|
||||
{
|
||||
return (CreateTypedGetterDelegate<Guid>(declaringType, prop), PropertyAccessorType.Guid);
|
||||
}
|
||||
|
||||
// Create typed getters for common value types
|
||||
var typeCode = Type.GetTypeCode(actualType);
|
||||
return typeCode switch
|
||||
{
|
||||
TypeCode.Int32 => (CreateTypedGetterDelegate<int>(declaringType, prop), PropertyAccessorType.Int32),
|
||||
TypeCode.Int64 => (CreateTypedGetterDelegate<long>(declaringType, prop), PropertyAccessorType.Int64),
|
||||
TypeCode.Boolean => (CreateTypedGetterDelegate<bool>(declaringType, prop), PropertyAccessorType.Boolean),
|
||||
TypeCode.Double => (CreateTypedGetterDelegate<double>(declaringType, prop), PropertyAccessorType.Double),
|
||||
TypeCode.Single => (CreateTypedGetterDelegate<float>(declaringType, prop), PropertyAccessorType.Single),
|
||||
TypeCode.Decimal => (CreateTypedGetterDelegate<decimal>(declaringType, prop), PropertyAccessorType.Decimal),
|
||||
TypeCode.DateTime => (CreateTypedGetterDelegate<DateTime>(declaringType, prop), PropertyAccessorType.DateTime),
|
||||
TypeCode.Byte => (CreateTypedGetterDelegate<byte>(declaringType, prop), PropertyAccessorType.Byte),
|
||||
TypeCode.Int16 => (CreateTypedGetterDelegate<short>(declaringType, prop), PropertyAccessorType.Int16),
|
||||
TypeCode.UInt16 => (CreateTypedGetterDelegate<ushort>(declaringType, prop), PropertyAccessorType.UInt16),
|
||||
TypeCode.UInt32 => (CreateTypedGetterDelegate<uint>(declaringType, prop), PropertyAccessorType.UInt32),
|
||||
TypeCode.UInt64 => (CreateTypedGetterDelegate<ulong>(declaringType, prop), PropertyAccessorType.UInt64),
|
||||
_ => (null, PropertyAccessorType.Object)
|
||||
};
|
||||
}
|
||||
|
||||
private static Delegate CreateTypedGetterDelegate<T>(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
return Expression.Lambda<Func<object, T>>(propAccess, objParam).Compile();
|
||||
}
|
||||
|
||||
private static Delegate CreateEnumGetterDelegate(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
var propAccess = Expression.Property(castExpr, prop);
|
||||
// Convert enum to int
|
||||
var convertToInt = Expression.Convert(propAccess, typeof(int));
|
||||
return Expression.Lambda<Func<object, int>>(convertToInt, objParam).Compile();
|
||||
}
|
||||
|
||||
private static Func<object, object?> CreateObjectGetter(Type declaringType, PropertyInfo prop)
|
||||
{
|
||||
var objParam = Expression.Parameter(typeof(object), "obj");
|
||||
var castExpr = Expression.Convert(objParam, declaringType);
|
||||
|
|
@ -866,9 +970,78 @@ public static class AcBinarySerializer
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object? GetValue(object obj) => _getter(obj);
|
||||
public object? GetValue(object obj) => _objectGetter(obj);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the accessor type for optimized writing without boxing.
|
||||
/// </summary>
|
||||
public PropertyAccessorType AccessorType => _accessorType;
|
||||
|
||||
// Typed getter methods - these avoid boxing
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long GetInt64(object obj) => ((Func<object, long>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool GetBoolean(object obj) => ((Func<object, bool>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double GetDouble(object obj) => ((Func<object, double>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public float GetSingle(object obj) => ((Func<object, float>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public decimal GetDecimal(object obj) => ((Func<object, decimal>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public DateTime GetDateTime(object obj) => ((Func<object, DateTime>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public byte GetByte(object obj) => ((Func<object, byte>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public short GetInt16(object obj) => ((Func<object, short>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ushort GetUInt16(object obj) => ((Func<object, ushort>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetUInt32(object obj) => ((Func<object, uint>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ulong GetUInt64(object obj) => ((Func<object, ulong>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Guid GetGuid(object obj) => ((Func<object, Guid>)_typedGetter!)(obj);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int GetEnumAsInt32(object obj) => ((Func<object, int>)_typedGetter!)(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the type of property accessor for optimized dispatch.
|
||||
/// </summary>
|
||||
internal enum PropertyAccessorType : byte
|
||||
{
|
||||
Object = 0,
|
||||
Int32,
|
||||
Int64,
|
||||
Boolean,
|
||||
Double,
|
||||
Single,
|
||||
Decimal,
|
||||
DateTime,
|
||||
Byte,
|
||||
Int16,
|
||||
UInt16,
|
||||
UInt32,
|
||||
UInt64,
|
||||
Guid,
|
||||
Enum
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Context Pool
|
||||
|
|
@ -962,15 +1135,43 @@ public static class AcBinarySerializer
|
|||
{
|
||||
_position = 0;
|
||||
_nextRefId = 1;
|
||||
_scanOccurrences?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
_multiReferenced?.Clear();
|
||||
_internedStrings?.Clear();
|
||||
|
||||
// Clear collections and trim if they grew too large
|
||||
// This prevents memory bloat when reusing pooled contexts
|
||||
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||
|
||||
_internedStringList?.Clear();
|
||||
_propertyNames?.Clear();
|
||||
_propertyNameList?.Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity) where TKey : notnull
|
||||
{
|
||||
if (dict == null) return;
|
||||
dict.Clear();
|
||||
// TrimExcess only if the dictionary grew significantly beyond initial capacity
|
||||
if (dict.EnsureCapacity(0) > maxCapacity)
|
||||
{
|
||||
dict.TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
|
||||
{
|
||||
if (set == null) return;
|
||||
set.Clear();
|
||||
// TrimExcess only if the set grew significantly beyond initial capacity
|
||||
if (set.EnsureCapacity(0) > maxCapacity)
|
||||
{
|
||||
set.TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
|
|
@ -1160,6 +1361,107 @@ public static class AcBinarySerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Bulk Array Writers
|
||||
|
||||
/// <summary>
|
||||
/// Writes int32 array with optimized tiny int encoding.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInt32ArrayOptimized(int[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes long array with optimized encoding (falls back to int32 when possible).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteLongArrayOptimized(long[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
var intValue = (int)value;
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(intValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int64);
|
||||
WriteVarLong(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes double array as bulk raw bytes - most efficient for large arrays.
|
||||
/// Each double is written with type code prefix for deserializer compatibility.
|
||||
/// </summary>
|
||||
public void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
// Each double needs 1 byte type code + 8 bytes data = 9 bytes
|
||||
EnsureCapacity(array.Length * 9);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes float array as bulk raw bytes.
|
||||
/// </summary>
|
||||
public void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
// Each float needs 1 byte type code + 4 bytes data = 5 bytes
|
||||
EnsureCapacity(array.Length * 5);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes Guid array as bulk raw bytes.
|
||||
/// </summary>
|
||||
public void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
// Each Guid needs 1 byte type code + 16 bytes data = 17 bytes
|
||||
EnsureCapacity(array.Length * 17);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header and Metadata
|
||||
|
||||
private int _headerPosition;
|
||||
|
|
@ -1210,11 +1512,15 @@ public static class AcBinarySerializer
|
|||
|
||||
#region Reference Handling
|
||||
|
||||
// Smaller initial capacity to reduce memory allocation when not many references
|
||||
private const int InitialReferenceCapacity = 16;
|
||||
private const int InitialMultiRefCapacity = 8;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
||||
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
||||
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
|
||||
|
||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
|
|
@ -1262,6 +1568,9 @@ public static class AcBinarySerializer
|
|||
|
||||
#region String Interning
|
||||
|
||||
// Smaller initial capacity for string interning
|
||||
private const int InitialInternCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetInternedStringIndex(string value, out int index)
|
||||
{
|
||||
|
|
@ -1274,8 +1583,8 @@ public static class AcBinarySerializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterInternedString(string value)
|
||||
{
|
||||
_internedStrings ??= new Dictionary<string, int>(32, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(32);
|
||||
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||
|
||||
if (!_internedStrings.ContainsKey(value))
|
||||
{
|
||||
|
|
@ -1289,11 +1598,14 @@ public static class AcBinarySerializer
|
|||
|
||||
#region Property Names
|
||||
|
||||
// Smaller initial capacity for property names
|
||||
private const int InitialPropertyNameCapacity = 16;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterPropertyName(string name)
|
||||
{
|
||||
_propertyNames ??= new Dictionary<string, int>(64, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(64);
|
||||
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
||||
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
||||
|
||||
if (!_propertyNames.ContainsKey(name))
|
||||
{
|
||||
|
|
@ -1316,6 +1628,181 @@ public static class AcBinarySerializer
|
|||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteTo(IBufferWriter<byte> writer)
|
||||
{
|
||||
// Directly write the internal buffer to the IBufferWriter
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_position);
|
||||
}
|
||||
|
||||
public int Position => _position;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Specialized Array Writers
|
||||
|
||||
/// <summary>
|
||||
/// Optimized array writer with specialized paths for primitive arrays.
|
||||
/// </summary>
|
||||
private static void WriteArray(IEnumerable enumerable, Type type, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Array);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
// Optimized path for primitive arrays
|
||||
var elementType = GetCollectionElementType(type);
|
||||
if (elementType != null && type.IsArray)
|
||||
{
|
||||
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
||||
return;
|
||||
}
|
||||
|
||||
// For IList, we can write the count directly
|
||||
if (enumerable is IList list)
|
||||
{
|
||||
var count = list.Count;
|
||||
context.WriteVarUInt((uint)count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = list[i];
|
||||
var itemType = item?.GetType() ?? typeof(object);
|
||||
WriteValue(item, itemType, context, nextDepth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For other IEnumerable, collect first
|
||||
var items = new List<object?>();
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
context.WriteVarUInt((uint)items.Count);
|
||||
foreach (var item in items)
|
||||
{
|
||||
var itemType = item?.GetType() ?? typeof(object);
|
||||
WriteValue(item, itemType, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specialized array writer for primitive arrays using bulk memory operations.
|
||||
/// Optimized for Blazor Hybrid compatibility (WASM, Android, Windows, iOS).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryWritePrimitiveArray(IEnumerable enumerable, Type elementType, BinarySerializationContext context)
|
||||
{
|
||||
// Int32 array - very common case
|
||||
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)intArray.Length);
|
||||
context.WriteInt32ArrayOptimized(intArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Double array - bulk write as raw bytes
|
||||
if (ReferenceEquals(elementType, DoubleType) && enumerable is double[] doubleArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)doubleArray.Length);
|
||||
context.WriteDoubleArrayBulk(doubleArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Long array
|
||||
if (ReferenceEquals(elementType, LongType) && enumerable is long[] longArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)longArray.Length);
|
||||
context.WriteLongArrayOptimized(longArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Float array - bulk write as raw bytes
|
||||
if (ReferenceEquals(elementType, FloatType) && enumerable is float[] floatArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)floatArray.Length);
|
||||
context.WriteFloatArrayBulk(floatArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bool array - pack as bytes
|
||||
if (ReferenceEquals(elementType, BoolType) && enumerable is bool[] boolArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)boolArray.Length);
|
||||
for (var i = 0; i < boolArray.Length; i++)
|
||||
{
|
||||
context.WriteByte(boolArray[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Guid array - bulk write
|
||||
if (ReferenceEquals(elementType, GuidType) && enumerable is Guid[] guidArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)guidArray.Length);
|
||||
context.WriteGuidArrayBulk(guidArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decimal array
|
||||
if (ReferenceEquals(elementType, DecimalType) && enumerable is decimal[] decimalArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)decimalArray.Length);
|
||||
for (var i = 0; i < decimalArray.Length; i++)
|
||||
{
|
||||
WriteDecimalUnsafe(decimalArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// DateTime array
|
||||
if (ReferenceEquals(elementType, DateTimeType) && enumerable is DateTime[] dateTimeArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)dateTimeArray.Length);
|
||||
for (var i = 0; i < dateTimeArray.Length; i++)
|
||||
{
|
||||
WriteDateTimeUnsafe(dateTimeArray[i], context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// String array - common case
|
||||
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
|
||||
{
|
||||
context.WriteVarUInt((uint)stringArray.Length);
|
||||
for (var i = 0; i < stringArray.Length; i++)
|
||||
{
|
||||
var s = stringArray[i];
|
||||
if (s == null)
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
else
|
||||
WriteString(s, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void WriteDictionary(IDictionary dictionary, BinarySerializationContext context, int depth)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.Dictionary);
|
||||
context.WriteVarUInt((uint)dictionary.Count);
|
||||
var nextDepth = depth + 1;
|
||||
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
// Write key
|
||||
var keyType = entry.Key?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Key, keyType, context, nextDepth);
|
||||
|
||||
// Write value
|
||||
var valueType = entry.Value?.GetType() ?? typeof(object);
|
||||
WriteValue(entry.Value, valueType, context, nextDepth);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ public class AcBinaryVsMessagePackFullBenchmark
|
|||
[Benchmark(Description = "AcBinary Populate WithRef")]
|
||||
public void Populate_AcBinary_WithRef()
|
||||
{
|
||||
// Create fresh target each time to avoid state accumulation
|
||||
var target = CreatePopulateTarget();
|
||||
AcBinaryDeserializer.Populate(_acBinaryWithRef, target);
|
||||
}
|
||||
|
|
@ -311,6 +312,7 @@ public class AcBinaryVsMessagePackFullBenchmark
|
|||
[Benchmark(Description = "AcBinary PopulateMerge WithRef")]
|
||||
public void PopulateMerge_AcBinary_WithRef()
|
||||
{
|
||||
// Create fresh target each time to avoid state accumulation
|
||||
var target = CreatePopulateTarget();
|
||||
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue