Unify and optimize primitive array/list serialization
Refactor AcBinarySerializer to use ReadOnlySpan<T> for bulk writing of primitive arrays and List<T>, replacing multiple specialized methods with a single TryWritePrimitiveCollection. This improves efficiency and reduces code duplication. Change default string interning mode to Attribute (opt-in). Update generated code path to allow reference tracking but not string interning. Adjust benchmarks to test correct serializer options. Reorder options for clarity.
This commit is contained in:
parent
dcd44cf705
commit
7284856dda
|
|
@ -212,15 +212,15 @@ public static class Program
|
|||
{
|
||||
|
||||
// AcBinary variants
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||
//new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
||||
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
||||
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
||||
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
||||
|
||||
// MemoryPack
|
||||
new MemoryPackBenchmark(testData.Order, SerializerMemoryPack),
|
||||
|
|
|
|||
|
|
@ -558,44 +558,44 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#region Bulk Array Writes — inline
|
||||
|
||||
public void WriteDoubleArrayBulk(double[] array)
|
||||
public void WriteDoubleArrayBulk(ReadOnlySpan<double> span)
|
||||
{
|
||||
EnsureCapacity(array.Length * 9);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
EnsureCapacity(span.Length * 9);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
|
||||
_position += 8;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteFloatArrayBulk(float[] array)
|
||||
public void WriteFloatArrayBulk(ReadOnlySpan<float> span)
|
||||
{
|
||||
EnsureCapacity(array.Length * 5);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
EnsureCapacity(span.Length * 5);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
|
||||
_position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteGuidArrayBulk(Guid[] array)
|
||||
public void WriteGuidArrayBulk(ReadOnlySpan<Guid> span)
|
||||
{
|
||||
EnsureCapacity(array.Length * 17);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
EnsureCapacity(span.Length * 17);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
span[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteInt32ArrayOptimized(int[] array)
|
||||
public void WriteInt32ArrayOptimized(ReadOnlySpan<int> span)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
var value = span[i];
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
|
|
@ -608,11 +608,11 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
public void WriteLongArrayOptimized(long[] array)
|
||||
public void WriteLongArrayOptimized(ReadOnlySpan<long> span)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
var value = span[i];
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
var intValue = (int)value;
|
||||
|
|
|
|||
|
|
@ -1047,12 +1047,13 @@ public static partial class AcBinarySerializer
|
|||
var hasPropertyFilter = context.HasPropertyFilter;
|
||||
|
||||
// Source-generated fast path: bypass the entire switch/delegate loop.
|
||||
// Only when no caching features are active (no string interning, no reference handling)
|
||||
// to avoid scan pass / write pass mismatch with interned strings and tracked references.
|
||||
// Reference handling is safe: ref tracking happens in WriteObject (before WriteProperties)
|
||||
// and child objects go through WriteValueGenerated → WriteObject → runtime ref tracking.
|
||||
// String interning is NOT safe: generated code doesn't set StringInternEligible → cursor mismatch.
|
||||
if (context.UseGeneratedCode)
|
||||
{
|
||||
var generatedWriter = wrapper.GeneratedWriter;
|
||||
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.HasCaching)
|
||||
if (generatedWriter != null && !hasPropertyFilter && !context.UseMetadata && !context.UseStringInterning)
|
||||
{
|
||||
generatedWriter.WriteProperties(value, context, nextDepth);
|
||||
return;
|
||||
|
|
@ -1493,7 +1494,7 @@ public static partial class AcBinarySerializer
|
|||
#region Specialized Array Writers
|
||||
|
||||
/// <summary>
|
||||
/// Optimized array writer with specialized paths for primitive arrays.
|
||||
/// Optimized array writer with specialized paths for primitive collections.
|
||||
/// </summary>
|
||||
private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
|
|
@ -1505,10 +1506,10 @@ public static partial class AcBinarySerializer
|
|||
var metadata = wrapper.Metadata;
|
||||
var elementType = metadata.CollectionElementType;
|
||||
|
||||
// Optimized path for primitive arrays
|
||||
if (elementType != null && metadata.SourceType.IsArray)
|
||||
// Optimized path for primitive T[] and List<T> — unified via ReadOnlySpan<T>
|
||||
if (elementType != null)
|
||||
{
|
||||
if (TryWritePrimitiveArray(enumerable, elementType, context))
|
||||
if (TryWritePrimitiveCollection(enumerable, elementType, context))
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1542,18 +1543,123 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specialized array writer for primitive arrays using bulk memory operations.
|
||||
/// Unified primitive collection writer for both T[] and List<T>.
|
||||
/// Extracts ReadOnlySpan<T> from either source (zero-cost implicit conversion for arrays,
|
||||
/// CollectionsMarshal.AsSpan for lists) and writes using bulk context methods with EnsureCapacity.
|
||||
/// </summary>
|
||||
private static bool TryWritePrimitiveArray<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
||||
private static bool TryWritePrimitiveCollection<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
// String array needs context for interning — keep generic path
|
||||
if (ReferenceEquals(elementType, StringType) && enumerable is string[] stringArray)
|
||||
if (ReferenceEquals(elementType, IntType))
|
||||
{
|
||||
context.WriteVarUInt((uint)stringArray.Length);
|
||||
for (var i = 0; i < stringArray.Length; i++)
|
||||
ReadOnlySpan<int> span;
|
||||
if (enumerable is int[] arr) span = arr;
|
||||
else if (enumerable is List<int> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
context.WriteInt32ArrayOptimized(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, LongType))
|
||||
{
|
||||
var s = stringArray[i];
|
||||
ReadOnlySpan<long> span;
|
||||
if (enumerable is long[] arr) span = arr;
|
||||
else if (enumerable is List<long> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
context.WriteLongArrayOptimized(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, DoubleType))
|
||||
{
|
||||
ReadOnlySpan<double> span;
|
||||
if (enumerable is double[] arr) span = arr;
|
||||
else if (enumerable is List<double> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
context.WriteDoubleArrayBulk(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, FloatType))
|
||||
{
|
||||
ReadOnlySpan<float> span;
|
||||
if (enumerable is float[] arr) span = arr;
|
||||
else if (enumerable is List<float> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
context.WriteFloatArrayBulk(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, BoolType))
|
||||
{
|
||||
ReadOnlySpan<bool> span;
|
||||
if (enumerable is bool[] arr) span = arr;
|
||||
else if (enumerable is List<bool> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
context.WriteByte(span[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, GuidType))
|
||||
{
|
||||
ReadOnlySpan<Guid> span;
|
||||
if (enumerable is Guid[] arr) span = arr;
|
||||
else if (enumerable is List<Guid> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
context.WriteGuidArrayBulk(span);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, DecimalType))
|
||||
{
|
||||
ReadOnlySpan<decimal> span;
|
||||
if (enumerable is decimal[] arr) span = arr;
|
||||
else if (enumerable is List<decimal> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
WriteDecimalUnsafe(span[i], context);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, DateTimeType))
|
||||
{
|
||||
ReadOnlySpan<DateTime> span;
|
||||
if (enumerable is DateTime[] arr) span = arr;
|
||||
else if (enumerable is List<DateTime> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
WriteDateTimeUnsafe(span[i], context);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(elementType, StringType))
|
||||
{
|
||||
ReadOnlySpan<string?> span;
|
||||
if (enumerable is string?[] arr) span = arr;
|
||||
else if (enumerable is List<string?> list) span = CollectionsMarshal.AsSpan(list);
|
||||
else return false;
|
||||
|
||||
context.WriteVarUInt((uint)span.Length);
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
var s = span[i];
|
||||
if (s == null)
|
||||
context.WriteByte(BinaryTypeCode.Null);
|
||||
else
|
||||
|
|
@ -1562,89 +1668,6 @@ public static partial class AcBinarySerializer
|
|||
return true;
|
||||
}
|
||||
|
||||
// All other primitive arrays — inline write through context (zero virtual dispatch)
|
||||
return TryWritePrimitiveArrayCore(enumerable, elementType, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core primitive array writes. Uses context write methods (zero virtual dispatch).
|
||||
/// </summary>
|
||||
private static bool TryWritePrimitiveArrayCore<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
|
||||
where TOutput : struct, IBinaryOutputBase
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,14 +86,6 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
|
||||
public bool UseGeneratedCode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||
/// Throws exception if FNV-1a hash collision is detected between property names of the same type.
|
||||
/// Should be enabled during development/testing, can be disabled in production for performance.
|
||||
/// Default: true (safety first)
|
||||
/// </summary>
|
||||
public bool CheckDuplicatePropName { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Controls how string interning is applied during serialization.
|
||||
/// None: No interning, all strings written inline.
|
||||
|
|
@ -101,7 +93,15 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// All: All strings within length limits are interned (legacy behavior).
|
||||
/// Default: All
|
||||
/// </summary>
|
||||
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.All;
|
||||
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute;
|
||||
|
||||
/// <summary>
|
||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||
/// Throws exception if FNV-1a hash collision is detected between property names of the same type.
|
||||
/// Should be enabled during development/testing, can be disabled in production for performance.
|
||||
/// Default: true (safety first)
|
||||
/// </summary>
|
||||
public bool CheckDuplicatePropName { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum string length to consider for interning.
|
||||
|
|
|
|||
Loading…
Reference in New Issue