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>
|
<ItemGroup>
|
||||||
<PackageReference Include="AutoMapper" Version="15.0.1" />
|
<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.EnvironmentVariables" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.11" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,80 @@ public static class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public static void Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
public static void Serialize<T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
|
||||||
{
|
{
|
||||||
var bytes = Serialize(value, options);
|
if (value == null)
|
||||||
writer.Write(bytes);
|
{
|
||||||
|
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
|
#endregion
|
||||||
|
|
@ -460,7 +528,8 @@ public static class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void WriteEnum(object value, BinarySerializationContext context)
|
private static void WriteEnum(object value, BinarySerializationContext context
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var intValue = Convert.ToInt32(value);
|
var intValue = Convert.ToInt32(value);
|
||||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||||
|
|
@ -534,29 +603,28 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
var metadata = GetTypeMetadata(type);
|
var metadata = GetTypeMetadata(type);
|
||||||
var nextDepth = depth + 1;
|
var nextDepth = depth + 1;
|
||||||
|
|
||||||
// Pre-count non-null, non-default properties
|
|
||||||
var writtenCount = 0;
|
|
||||||
var properties = metadata.Properties;
|
var properties = metadata.Properties;
|
||||||
var propCount = properties.Length;
|
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++)
|
for (var i = 0; i < propCount; i++)
|
||||||
{
|
{
|
||||||
var prop = properties[i];
|
var prop = properties[i];
|
||||||
var propValue = prop.GetValue(value);
|
if (IsPropertyDefaultOrNull(value, prop))
|
||||||
if (propValue == null) continue;
|
continue;
|
||||||
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
|
validIndices[writtenCount++] = i;
|
||||||
writtenCount++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.WriteVarUInt((uint)writtenCount);
|
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 prop = properties[validIndices[j]];
|
||||||
var propValue = prop.GetValue(value);
|
|
||||||
if (propValue == null) continue;
|
|
||||||
if (IsDefaultValueFast(propValue, prop.TypeCode, prop.PropertyType)) continue;
|
|
||||||
|
|
||||||
// Write property index or name
|
// Write property index or name
|
||||||
if (context.UseMetadata)
|
if (context.UseMetadata)
|
||||||
|
|
@ -569,167 +637,124 @@ public static class AcBinarySerializer
|
||||||
WriteString(prop.Name, context);
|
WriteString(prop.Name, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteValue(propValue, prop.PropertyType, context, nextDepth);
|
// Use typed writers to avoid boxing
|
||||||
|
WritePropertyValue(value, prop, context, nextDepth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optimized array writer with specialized paths for primitive arrays.
|
/// Checks if a property value is null or default without boxing for value types.
|
||||||
/// </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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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
|
switch (prop.AccessorType)
|
||||||
if (ReferenceEquals(elementType, IntType) && enumerable is int[] intArray)
|
|
||||||
{
|
{
|
||||||
context.WriteVarUInt((uint)intArray.Length);
|
case PropertyAccessorType.Int32:
|
||||||
for (var i = 0; i < intArray.Length; i++)
|
return prop.GetInt32(obj) == 0;
|
||||||
{
|
case PropertyAccessorType.Int64:
|
||||||
WriteInt32(intArray[i], context);
|
return prop.GetInt64(obj) == 0L;
|
||||||
}
|
case PropertyAccessorType.Boolean:
|
||||||
return true;
|
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);
|
switch (prop.AccessorType)
|
||||||
context.WriteVarUInt((uint)dictionary.Count);
|
|
||||||
var nextDepth = depth + 1;
|
|
||||||
|
|
||||||
foreach (DictionaryEntry entry in dictionary)
|
|
||||||
{
|
{
|
||||||
// Write key
|
case PropertyAccessorType.Int32:
|
||||||
var keyType = entry.Key?.GetType() ?? typeof(object);
|
WriteInt32(prop.GetInt32(obj), context);
|
||||||
WriteValue(entry.Key, keyType, context, nextDepth);
|
return;
|
||||||
|
case PropertyAccessorType.Int64:
|
||||||
// Write value
|
WriteInt64(prop.GetInt64(obj), context);
|
||||||
var valueType = entry.Value?.GetType() ?? typeof(object);
|
return;
|
||||||
WriteValue(entry.Value, valueType, context, nextDepth);
|
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
|
internal sealed class BinaryPropertyAccessor
|
||||||
{
|
{
|
||||||
public readonly string Name;
|
public readonly string Name;
|
||||||
public readonly byte[] NameUtf8;
|
public readonly byte[] NameUtf8;
|
||||||
public readonly Type PropertyType;
|
public readonly Type PropertyType;
|
||||||
public readonly TypeCode TypeCode;
|
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)
|
public BinaryPropertyAccessor(PropertyInfo prop)
|
||||||
{
|
{
|
||||||
|
|
@ -853,10 +887,80 @@ public static class AcBinarySerializer
|
||||||
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
|
NameUtf8 = Encoding.UTF8.GetBytes(prop.Name);
|
||||||
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
PropertyType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||||
TypeCode = Type.GetTypeCode(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 objParam = Expression.Parameter(typeof(object), "obj");
|
||||||
var castExpr = Expression.Convert(objParam, declaringType);
|
var castExpr = Expression.Convert(objParam, declaringType);
|
||||||
|
|
@ -866,9 +970,78 @@ public static class AcBinarySerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[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
|
#endregion
|
||||||
|
|
||||||
#region Context Pool
|
#region Context Pool
|
||||||
|
|
@ -962,15 +1135,43 @@ public static class AcBinarySerializer
|
||||||
{
|
{
|
||||||
_position = 0;
|
_position = 0;
|
||||||
_nextRefId = 1;
|
_nextRefId = 1;
|
||||||
_scanOccurrences?.Clear();
|
|
||||||
_writtenRefs?.Clear();
|
// Clear collections and trim if they grew too large
|
||||||
_multiReferenced?.Clear();
|
// This prevents memory bloat when reusing pooled contexts
|
||||||
_internedStrings?.Clear();
|
ClearAndTrimIfNeeded(_scanOccurrences, InitialReferenceCapacity * 4);
|
||||||
|
ClearAndTrimIfNeeded(_writtenRefs, InitialReferenceCapacity * 4);
|
||||||
|
ClearAndTrimIfNeeded(_multiReferenced, InitialMultiRefCapacity * 4);
|
||||||
|
ClearAndTrimIfNeeded(_internedStrings, InitialInternCapacity * 4);
|
||||||
|
ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4);
|
||||||
|
|
||||||
_internedStringList?.Clear();
|
_internedStringList?.Clear();
|
||||||
_propertyNames?.Clear();
|
|
||||||
_propertyNameList?.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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_buffer != null)
|
if (_buffer != null)
|
||||||
|
|
@ -1160,6 +1361,107 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
#endregion
|
#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
|
#region Header and Metadata
|
||||||
|
|
||||||
private int _headerPosition;
|
private int _headerPosition;
|
||||||
|
|
@ -1210,11 +1512,15 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
#region Reference Handling
|
#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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TrackForScanning(object obj)
|
public bool TrackForScanning(object obj)
|
||||||
{
|
{
|
||||||
_scanOccurrences ??= new Dictionary<object, int>(64, ReferenceEqualityComparer.Instance);
|
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
||||||
_multiReferenced ??= new HashSet<object>(32, ReferenceEqualityComparer.Instance);
|
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
ref var count = ref CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||||
if (exists)
|
if (exists)
|
||||||
|
|
@ -1262,6 +1568,9 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
#region String Interning
|
#region String Interning
|
||||||
|
|
||||||
|
// Smaller initial capacity for string interning
|
||||||
|
private const int InitialInternCapacity = 16;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetInternedStringIndex(string value, out int index)
|
public bool TryGetInternedStringIndex(string value, out int index)
|
||||||
{
|
{
|
||||||
|
|
@ -1274,8 +1583,8 @@ public static class AcBinarySerializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void RegisterInternedString(string value)
|
public void RegisterInternedString(string value)
|
||||||
{
|
{
|
||||||
_internedStrings ??= new Dictionary<string, int>(32, StringComparer.Ordinal);
|
_internedStrings ??= new Dictionary<string, int>(InitialInternCapacity, StringComparer.Ordinal);
|
||||||
_internedStringList ??= new List<string>(32);
|
_internedStringList ??= new List<string>(InitialInternCapacity);
|
||||||
|
|
||||||
if (!_internedStrings.ContainsKey(value))
|
if (!_internedStrings.ContainsKey(value))
|
||||||
{
|
{
|
||||||
|
|
@ -1289,11 +1598,14 @@ public static class AcBinarySerializer
|
||||||
|
|
||||||
#region Property Names
|
#region Property Names
|
||||||
|
|
||||||
|
// Smaller initial capacity for property names
|
||||||
|
private const int InitialPropertyNameCapacity = 16;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void RegisterPropertyName(string name)
|
public void RegisterPropertyName(string name)
|
||||||
{
|
{
|
||||||
_propertyNames ??= new Dictionary<string, int>(64, StringComparer.Ordinal);
|
_propertyNames ??= new Dictionary<string, int>(InitialPropertyNameCapacity, StringComparer.Ordinal);
|
||||||
_propertyNameList ??= new List<string>(64);
|
_propertyNameList ??= new List<string>(InitialPropertyNameCapacity);
|
||||||
|
|
||||||
if (!_propertyNames.ContainsKey(name))
|
if (!_propertyNames.ContainsKey(name))
|
||||||
{
|
{
|
||||||
|
|
@ -1316,6 +1628,181 @@ public static class AcBinarySerializer
|
||||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||||
return 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
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,7 @@ public class AcBinaryVsMessagePackFullBenchmark
|
||||||
[Benchmark(Description = "AcBinary Populate WithRef")]
|
[Benchmark(Description = "AcBinary Populate WithRef")]
|
||||||
public void Populate_AcBinary_WithRef()
|
public void Populate_AcBinary_WithRef()
|
||||||
{
|
{
|
||||||
|
// Create fresh target each time to avoid state accumulation
|
||||||
var target = CreatePopulateTarget();
|
var target = CreatePopulateTarget();
|
||||||
AcBinaryDeserializer.Populate(_acBinaryWithRef, target);
|
AcBinaryDeserializer.Populate(_acBinaryWithRef, target);
|
||||||
}
|
}
|
||||||
|
|
@ -311,6 +312,7 @@ public class AcBinaryVsMessagePackFullBenchmark
|
||||||
[Benchmark(Description = "AcBinary PopulateMerge WithRef")]
|
[Benchmark(Description = "AcBinary PopulateMerge WithRef")]
|
||||||
public void PopulateMerge_AcBinary_WithRef()
|
public void PopulateMerge_AcBinary_WithRef()
|
||||||
{
|
{
|
||||||
|
// Create fresh target each time to avoid state accumulation
|
||||||
var target = CreatePopulateTarget();
|
var target = CreatePopulateTarget();
|
||||||
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target);
|
AcBinaryDeserializer.PopulateMerge(_acBinaryWithRef.AsSpan(), target);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue