Enhance AcBinary: property filter, string interning, arrays
- Add property-level filtering via BinaryPropertyFilter delegate and context - Improve string interning with new StringInternNew type code and promotion logic - Optimize array and dictionary serialization for primitive types - Expose strongly-typed property accessors for primitives and enums - Add new benchmarks for serialization modes - Refactor buffer pooling and cleanup code - All new features are opt-in; maintains backward compatibility
This commit is contained in:
parent
5601c0d3e2
commit
271f23d0f6
|
|
@ -466,3 +466,79 @@ public class SizeComparisonBenchmark
|
||||||
[Benchmark(Description = "Placeholder")]
|
[Benchmark(Description = "Placeholder")]
|
||||||
public int Placeholder() => 1; // Just to make BenchmarkDotNet happy
|
public int Placeholder() => 1; // Just to make BenchmarkDotNet happy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum BinaryBenchmarkMode
|
||||||
|
{
|
||||||
|
Default,
|
||||||
|
NoReferenceHandling,
|
||||||
|
FastMode
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class AcBinaryOptionsBenchmarkBase
|
||||||
|
{
|
||||||
|
protected TestOrder TestOrder = null!;
|
||||||
|
protected AcBinarySerializerOptions BinaryOptions = null!;
|
||||||
|
protected MessagePackSerializerOptions MsgPackOptions = null!;
|
||||||
|
protected byte[] AcBinaryData = null!;
|
||||||
|
protected byte[] MsgPackData = null!;
|
||||||
|
|
||||||
|
[Params(BinaryBenchmarkMode.Default, BinaryBenchmarkMode.NoReferenceHandling, BinaryBenchmarkMode.FastMode)]
|
||||||
|
public BinaryBenchmarkMode Mode { get; set; }
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void GlobalSetup()
|
||||||
|
{
|
||||||
|
TestDataFactory.ResetIdCounter();
|
||||||
|
TestOrder = TestDataFactory.CreateBenchmarkOrder(
|
||||||
|
itemCount: 4,
|
||||||
|
palletsPerItem: 3,
|
||||||
|
measurementsPerPallet: 3,
|
||||||
|
pointsPerMeasurement: 6);
|
||||||
|
|
||||||
|
BinaryOptions = CreateBinaryOptions(Mode);
|
||||||
|
MsgPackOptions = ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);
|
||||||
|
|
||||||
|
AcBinaryData = AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
|
||||||
|
MsgPackData = MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
|
||||||
|
|
||||||
|
var ratio = MsgPackData.Length == 0 ? 0 : 100.0 * AcBinaryData.Length / MsgPackData.Length;
|
||||||
|
Console.WriteLine($"[BenchmarkSetup] Mode={Mode} | AcBinary={AcBinaryData.Length} bytes | MessagePack={MsgPackData.Length} bytes | Ratio={ratio:F1}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AcBinarySerializerOptions CreateBinaryOptions(BinaryBenchmarkMode mode) => mode switch
|
||||||
|
{
|
||||||
|
BinaryBenchmarkMode.Default => new AcBinarySerializerOptions(),
|
||||||
|
BinaryBenchmarkMode.NoReferenceHandling => AcBinarySerializerOptions.WithoutReferenceHandling(),
|
||||||
|
BinaryBenchmarkMode.FastMode => new AcBinarySerializerOptions
|
||||||
|
{
|
||||||
|
UseMetadata = false,
|
||||||
|
UseStringInterning = false,
|
||||||
|
UseReferenceHandling = false
|
||||||
|
},
|
||||||
|
_ => new AcBinarySerializerOptions()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShortRunJob]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[RankColumn]
|
||||||
|
public class AcBinaryOptionsSerializeBenchmark : AcBinaryOptionsBenchmarkBase
|
||||||
|
{
|
||||||
|
[Benchmark(Description = "MessagePack Serialize", Baseline = true)]
|
||||||
|
public byte[] Serialize_MessagePack() => MessagePackSerializer.Serialize(TestOrder, MsgPackOptions);
|
||||||
|
|
||||||
|
[Benchmark(Description = "AcBinary Serialize")]
|
||||||
|
public byte[] Serialize_AcBinary() => AcBinarySerializer.Serialize(TestOrder, BinaryOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ShortRunJob]
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[RankColumn]
|
||||||
|
public class AcBinaryOptionsDeserializeBenchmark : AcBinaryOptionsBenchmarkBase
|
||||||
|
{
|
||||||
|
[Benchmark(Description = "MessagePack Deserialize", Baseline = true)]
|
||||||
|
public TestOrder? Deserialize_MessagePack() => MessagePackSerializer.Deserialize<TestOrder>(MsgPackData, MsgPackOptions);
|
||||||
|
|
||||||
|
[Benchmark(Description = "AcBinary Deserialize")]
|
||||||
|
public TestOrder? Deserialize_AcBinary() => AcBinaryDeserializer.Deserialize<TestOrder>(AcBinaryData);
|
||||||
|
}
|
||||||
|
|
@ -67,9 +67,10 @@ public static class AcBinaryDeserializer
|
||||||
RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
|
RegisterReader(BinaryTypeCode.Float64, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDoubleUnsafe());
|
||||||
RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
|
RegisterReader(BinaryTypeCode.Decimal, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDecimalUnsafe());
|
||||||
RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
|
RegisterReader(BinaryTypeCode.Char, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadCharUnsafe());
|
||||||
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndInternString(ref ctx));
|
RegisterReader(BinaryTypeCode.String, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadPlainString(ref ctx));
|
||||||
RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
|
RegisterReader(BinaryTypeCode.StringInterned, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.GetInternedString((int)ctx.ReadVarUInt()));
|
||||||
RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
|
RegisterReader(BinaryTypeCode.StringEmpty, static (ref BinaryDeserializationContext _, Type _, int _) => string.Empty);
|
||||||
|
RegisterReader(BinaryTypeCode.StringInternNew, static (ref BinaryDeserializationContext ctx, Type _, int _) => ReadAndRegisterInternedString(ref ctx));
|
||||||
RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
|
RegisterReader(BinaryTypeCode.DateTime, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeUnsafe());
|
||||||
RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
|
RegisterReader(BinaryTypeCode.DateTimeOffset, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadDateTimeOffsetUnsafe());
|
||||||
RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
|
RegisterReader(BinaryTypeCode.TimeSpan, static (ref BinaryDeserializationContext ctx, Type _, int _) => ctx.ReadTimeSpanUnsafe());
|
||||||
|
|
@ -281,6 +282,30 @@ public static class AcBinaryDeserializer
|
||||||
context.Position, targetType);
|
context.Position, targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sima string olvasása - NEM regisztrál az intern táblába.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static string ReadPlainString(ref BinaryDeserializationContext context)
|
||||||
|
{
|
||||||
|
var length = (int)context.ReadVarUInt();
|
||||||
|
if (length == 0) return string.Empty;
|
||||||
|
return context.ReadStringUtf8(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Új internált string olvasása és regisztrálása az intern táblába.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static string ReadAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||||
|
{
|
||||||
|
var length = (int)context.ReadVarUInt();
|
||||||
|
if (length == 0) return string.Empty;
|
||||||
|
var str = context.ReadStringUtf8(length);
|
||||||
|
context.RegisterInternedString(str);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read a string and register it in the intern table for future references.
|
/// Read a string and register it in the intern table for future references.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1109,12 +1134,16 @@ public static class AcBinaryDeserializer
|
||||||
context.Skip(16);
|
context.Skip(16);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.String:
|
case BinaryTypeCode.String:
|
||||||
// CRITICAL FIX: Must register string in intern table even when skipping!
|
// Sima string - nem regisztrálunk
|
||||||
SkipAndInternString(ref context);
|
SkipPlainString(ref context);
|
||||||
return;
|
return;
|
||||||
case BinaryTypeCode.StringInterned:
|
case BinaryTypeCode.StringInterned:
|
||||||
context.ReadVarUInt();
|
context.ReadVarUInt();
|
||||||
return;
|
return;
|
||||||
|
case BinaryTypeCode.StringInternNew:
|
||||||
|
// Új internált string - regisztrálni kell még skip esetén is
|
||||||
|
SkipAndRegisterInternedString(ref context);
|
||||||
|
return;
|
||||||
case BinaryTypeCode.ByteArray:
|
case BinaryTypeCode.ByteArray:
|
||||||
var byteLen = (int)context.ReadVarUInt();
|
var byteLen = (int)context.ReadVarUInt();
|
||||||
context.Skip(byteLen);
|
context.Skip(byteLen);
|
||||||
|
|
@ -1139,6 +1168,31 @@ public static class AcBinaryDeserializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sima string kihagyása - NEM regisztrál.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void SkipPlainString(ref BinaryDeserializationContext context)
|
||||||
|
{
|
||||||
|
var byteLen = (int)context.ReadVarUInt();
|
||||||
|
if (byteLen > 0)
|
||||||
|
{
|
||||||
|
context.Skip(byteLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Új internált string kihagyása - DE regisztrálni kell!
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static void SkipAndRegisterInternedString(ref BinaryDeserializationContext context)
|
||||||
|
{
|
||||||
|
var byteLen = (int)context.ReadVarUInt();
|
||||||
|
if (byteLen == 0) return;
|
||||||
|
var str = context.ReadStringUtf8(byteLen);
|
||||||
|
context.RegisterInternedString(str);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Skip a string but still register it in the intern table if it meets the length threshold.
|
/// Skip a string but still register it in the intern table if it meets the length threshold.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AyCode.Core.Extensions;
|
namespace AyCode.Core.Extensions;
|
||||||
|
|
@ -69,6 +70,12 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int InitialBufferCapacity { get; init; } = 4096;
|
public int InitialBufferCapacity { get; init; } = 4096;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional property-level filter invoked before metadata registration and serialization.
|
||||||
|
/// Return false to exclude the property from the payload.
|
||||||
|
/// </summary>
|
||||||
|
public BinaryPropertyFilter? PropertyFilter { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates options with specified max depth.
|
/// Creates options with specified max depth.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -117,6 +124,7 @@ internal static class BinaryTypeCode
|
||||||
public const byte String = 16; // Inline UTF8 string
|
public const byte String = 16; // Inline UTF8 string
|
||||||
public const byte StringInterned = 17; // Reference to interned string by index
|
public const byte StringInterned = 17; // Reference to interned string by index
|
||||||
public const byte StringEmpty = 18; // Empty string marker
|
public const byte StringEmpty = 18; // Empty string marker
|
||||||
|
public const byte StringInternNew = 19; // New interned string - full content + register in table
|
||||||
|
|
||||||
// Date/Time types (20-23)
|
// Date/Time types (20-23)
|
||||||
public const byte DateTime = 20;
|
public const byte DateTime = 20;
|
||||||
|
|
@ -190,3 +198,62 @@ internal static class BinaryTypeCode
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delegate used to decide whether a property should be serialized.
|
||||||
|
/// </summary>
|
||||||
|
public delegate bool BinaryPropertyFilter(in BinaryPropertyFilterContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides property metadata and lazy value access for property filter evaluations.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct BinaryPropertyFilterContext
|
||||||
|
{
|
||||||
|
private readonly object? _instance;
|
||||||
|
private readonly Func<object, object?>? _valueGetter;
|
||||||
|
|
||||||
|
internal BinaryPropertyFilterContext(object? instance, Type declaringType, string propertyName, Type propertyType, Func<object, object?>? valueGetter)
|
||||||
|
{
|
||||||
|
_instance = instance;
|
||||||
|
DeclaringType = declaringType;
|
||||||
|
PropertyName = propertyName;
|
||||||
|
PropertyType = propertyType;
|
||||||
|
_valueGetter = valueGetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the declaring type of the property.
|
||||||
|
/// </summary>
|
||||||
|
public Type DeclaringType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the property name.
|
||||||
|
/// </summary>
|
||||||
|
public string PropertyName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the property type.
|
||||||
|
/// </summary>
|
||||||
|
public Type PropertyType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the instance being serialized when available. Null during metadata registration.
|
||||||
|
/// </summary>
|
||||||
|
public object? Instance => _instance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the filter is invoked during metadata registration (when no instance is available).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsMetadataPhase => _instance is null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lazily obtains the current property value. Returns null when invoked during metadata registration.
|
||||||
|
/// </summary>
|
||||||
|
public object? GetValue()
|
||||||
|
{
|
||||||
|
if (_instance == null || _valueGetter == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _valueGetter(_instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue