diff --git a/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs index 7e0d56a..6732513 100644 --- a/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBenchmark.cs @@ -29,6 +29,7 @@ public sealed class AcBinaryBenchmark : ISerializerBenchmark where T : class _order = order; _options = options; OptionsPreset = optionsPreset; + _serialized = AcBinarySerializer.Serialize(order, options); } @@ -42,6 +43,7 @@ public sealed class AcBinaryBenchmark : ISerializerBenchmark where T : class { var bytes = AcBinarySerializer.Serialize(_order, _options); var roundTripped = AcBinaryDeserializer.Deserialize(bytes, _options); + return RoundTripValidator.DeepEqualsViaJson(_order, roundTripped); } } diff --git a/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs index ffaf4e9..b4d86e6 100644 --- a/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs +++ b/AyCode.Benchmark/Workloads/Scenarios/AcBinaryBufferWriterBenchmark.cs @@ -31,6 +31,7 @@ public sealed class AcBinaryBufferWriterBenchmark : ISerializerBenchmark wher _order = order; _options = options; OptionsPreset = optionsPreset; + _serialized = AcBinarySerializer.Serialize(order, options); // Measure ONLY the BufferWriter infrastructure setup on the serialize side (excluding the diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index a0678e8..a8b4d67 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Runtime.CompilerServices; -using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Binaries; @@ -18,25 +17,20 @@ public static partial class AcBinarySerializer /// so no dictionary lookup overhead. SGen types call generated ScanForDuplicates /// which bypasses the entire runtime scan path. /// - private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context) + private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context, TypeMetadataWrapper? wrapper = null) where TOutput : struct, IBinaryOutputBase { - if (!context.HasCaching) - return; + if (!context.HasCaching) return; - var wrapper = context.GetWrapper(type); + wrapper ??= context.GetWrapper(type); // SGen path: wrapper.GeneratedWriter is cached (no registry lookup per call). // Generated ScanForDuplicates handles HasCaching + ScanObject + SortWritePlan. var genWriter = wrapper.GeneratedWriter; - if (genWriter != null && context.Options.UseGeneratedCode) - { - genWriter.ScanObject(value, context); - context.SortWritePlan(); - return; - } - ScanValue(value, wrapper, context); + if (genWriter != null && context.Options.UseGeneratedCode) genWriter.ScanObject(value, context); + else ScanValue(value, wrapper, context); + context.SortWritePlan(); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 189b11f..a69cca3 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -281,8 +281,7 @@ public static partial class AcBinarySerializer internal static void Register(Type type, IGeneratedBinaryWriter writer) => Writers[type] = writer; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static IGeneratedBinaryWriter? TryGet(Type type) => - Writers.TryGetValue(type, out var writer) ? writer : null; + internal static IGeneratedBinaryWriter? TryGet(Type type) => Writers.GetValueOrDefault(type); } /// @@ -312,85 +311,40 @@ public static partial class AcBinarySerializer /// Uses ArrayBinaryOutput for byte[] result path. /// public static byte[] Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, AcBinarySerializerOptions options) - { - if (value == null) return [BinaryTypeCode.Null]; + => Serialize(value, typeof(T), options); - var runtimeType = value.GetType(); - var context = AcquireArrayOutputContext(options); - - try - { - // SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain. - // If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection. - if (options.UseGeneratedCode) - { - var wrapper = context.GetWrapper(runtimeType); - if (wrapper.GeneratedWriter != null) - { - ScanForDuplicates(value, runtimeType, context); - context.WriteHeader(); - WriteObject(value, wrapper, context); - - if (options.UseCompression != Lz4CompressionMode.None) - return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); - return context.Output.ToArray(context._buffer, context._position); - } - } - - // Full path: IQueryable/Expression conversion, primitive/collection dispatch - var actualValue = ConvertExpressionValue(value, ref runtimeType); - ScanForDuplicates(actualValue, runtimeType, context); - context.WriteHeader(); - WriteValue(actualValue, runtimeType, context); - - if (options.UseCompression != Lz4CompressionMode.None) - return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); - return context.Output.ToArray(context._buffer, context._position); - } - finally - { - ReturnContext(context, options); - } - } + public static byte[] Serialize(object? value, AcBinarySerializerOptions options) + => value == null ? [BinaryTypeCode.Null] : Serialize(value, value.GetType(), options); /// /// Non-generic Type-based . For /// runtime-typed scenarios (plugin frameworks, ASP.NET ModelBinding, MVC formatters). The - /// parameter is the declared-type hint; the body uses - /// value.GetType() for the runtime polymorphism path, identical to the generic version. + /// parameter is the declared type used for dispatch — identical semantics + /// to the generic version where typeof(T) is the dispatch type. /// public static byte[] Serialize(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, AcBinarySerializerOptions options) { if (value == null) return [BinaryTypeCode.Null]; - var runtimeType = value.GetType(); var context = AcquireArrayOutputContext(options); try { - if (options.UseGeneratedCode) - { - var wrapper = context.GetWrapper(runtimeType); - if (wrapper.GeneratedWriter != null) - { - ScanForDuplicates(value, runtimeType, context); - context.WriteHeader(); - WriteObject(value, wrapper, context); + // Full path: IQueryable/Expression conversion, primitive/collection dispatch + var actualValue = value; //ConvertExpressionValue(value, ref runtimeType); + var wrapper = context.GetWrapper(type); - if (options.UseCompression != Lz4CompressionMode.None) - return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); - return context.Output.ToArray(context._buffer, context._position); - } - } - - var actualValue = ConvertExpressionValue(value, ref runtimeType); - ScanForDuplicates(actualValue, runtimeType, context); + ScanForDuplicates(actualValue, type, context, wrapper); context.WriteHeader(); - WriteValue(actualValue, runtimeType, context); - if (options.UseCompression != Lz4CompressionMode.None) - return Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression); - return context.Output.ToArray(context._buffer, context._position); + // SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain. + // If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection. + if (wrapper.GeneratedWriter != null && options.UseGeneratedCode) WriteObject(actualValue, wrapper, context); + else WriteValue(actualValue, type, context); + + return options.UseCompression != Lz4CompressionMode.None + ? Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression) + : context.Output.ToArray(context._buffer, context._position); } finally { @@ -405,8 +359,10 @@ public static partial class AcBinarySerializer internal static void ScanOnly(T value, AcBinarySerializerOptions options) { if (value == null) return; - var runtimeType = value.GetType(); + + var runtimeType = typeof(T); var context = AcquireArrayOutputContext(options); + try { ScanForDuplicates(value, runtimeType, context); @@ -432,8 +388,9 @@ public static partial class AcBinarySerializer return 1; } - var runtimeType = value.GetType(); + var runtimeType = typeof(T); var context = BinarySerializationContextPool.Get(options); + context.Output = new BufferWriterBinaryOutput(writer, options.BufferWriterChunkSize); context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); @@ -494,7 +451,7 @@ public static partial class AcBinarySerializer return 1; } - var runtimeType = value.GetType(); + var runtimeType = type; var context = BinarySerializationContextPool.Get(options); context.Output = new BufferWriterBinaryOutput(writer, options.BufferWriterChunkSize); context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd); @@ -568,11 +525,13 @@ public static partial class AcBinarySerializer /// /// Total serialized bytes written. public static int SerializeChunked<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null) - { - if (pipe is null) throw new ArgumentNullException(nameof(pipe)); - return SerializeToPipeWriterCore(value, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false); - } + => SerializeToPipeWriterCore(value, typeof(T), pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false); + // SerializeChunked non-generic + public static int SerializeChunked(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null) + => SerializeToPipeWriterCore(value, type, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false); + + /// /// Serialize to any as a chunked stream — pure /// AcBinary bytes, no per-chunk header. The output is byte-compatible with @@ -597,7 +556,7 @@ public static partial class AcBinarySerializer /// Serializer options (type wrappers, reference handling, interning, etc.). /// Total serialized bytes written. public static int SerializeChunked<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options) - => SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false); + => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false); /// /// Non-generic Type-based counterpart to @@ -605,7 +564,7 @@ public static partial class AcBinarySerializer /// For runtime-typed scenarios (MVC formatters, plugin frameworks). /// public static int SerializeChunked(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options) - => SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false); + => SerializeToPipeWriterCore(value,type, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false); /// /// Serialize a value into a chunked stream where each chunk carries a self-describing @@ -635,10 +594,8 @@ public static partial class AcBinarySerializer /// See . /// Total serialized data bytes (excluding framing overhead). public static int SerializeChunkedFramed<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null) - { - if (pipe is null) throw new ArgumentNullException(nameof(pipe)); - return SerializeToPipeWriterCore(value, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: true); - } + => SerializeToPipeWriterCore(value, typeof(T), pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: true); + /// /// Serialize to any with per-chunk frame headers @@ -650,14 +607,14 @@ public static partial class AcBinarySerializer /// . /// public static int SerializeChunkedFramed<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options) - => SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true); + => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true); /// /// Non-generic Type-based counterpart to /// . /// public static int SerializeChunkedFramed(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options) - => SerializeToPipeWriterCore(value, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true); + => SerializeToPipeWriterCore(value, type, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true); /// /// Internal flush-tunable framed PipeWriter overload — used by AyCode.Services @@ -666,7 +623,7 @@ public static partial class AcBinarySerializer /// on a guaranteed parallel-capable writer. /// internal static int SerializeChunkedFramed(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout) - => SerializeToPipeWriterCore(value, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true); + => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true); /// /// Internal legacy alias for @@ -675,14 +632,14 @@ public static partial class AcBinarySerializer /// (framed wire format with [201][UINT16][data] per chunk + [202] end marker). /// internal static int Serialize(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout) - => SerializeToPipeWriterCore(value, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true); + => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true); /// /// Common pipe-output serialization core. Same loop for both raw () /// and framed () modes — the only difference flows through /// into the ctor. /// - private static int SerializeToPipeWriterCore(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout, bool multiMessage) + private static int SerializeToPipeWriterCore(object? value, Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout, bool multiMessage) { if (value == null) { @@ -707,7 +664,7 @@ public static partial class AcBinarySerializer return 1; } - var runtimeType = value.GetType(); + var runtimeType = type; var context = BinarySerializationContextPool.Get(options); context.Output = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage, flushPolicy, flushTimeout); @@ -760,7 +717,7 @@ public static partial class AcBinarySerializer { if (value == null) return 1; - var runtimeType = value.GetType(); + var runtimeType = typeof(T); var context = AcquireArrayOutputContext(options); try @@ -785,7 +742,7 @@ public static partial class AcBinarySerializer { if (value == null) return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]); - var runtimeType = value.GetType(); + var runtimeType = typeof(T); var context = AcquireArrayOutputContext(options); try @@ -983,7 +940,7 @@ public static partial class AcBinarySerializer // Only Nullable can be a value type in the Object accessor path. if (type.IsValueType) { - if (TryWritePrimitive(value, value.GetType(), context)) + if (TryWritePrimitive(value, type, context)) return; }