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:
Loretta 2025-12-14 12:45:29 +01:00
parent 5601c0d3e2
commit 271f23d0f6
4 changed files with 1217 additions and 977 deletions

View File

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

View File

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

View File

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