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:
Loretta 2025-12-13 03:25:02 +01:00
parent f69b14c195
commit 056aae97a5
3 changed files with 669 additions and 180 deletions

View File

@ -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" />

View File

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

View File

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