[LOADED_DOCS: 3 files, no new loads]

NativeAOT: fallback for delegates, exclude MessagePack

Added AYCODE_NATIVEAOT symbol for AOT builds and excluded MessagePack benchmarks from NativeAOT due to lack of AOT support. Updated AcSerializerCommon to use reflection-based delegates when dynamic code is unavailable, ensuring compatibility with both JIT and AOT. Added explanatory comments throughout.
This commit is contained in:
Loretta 2026-05-03 19:09:25 +02:00
parent 97ac3e21a3
commit 9b8e56557f
3 changed files with 53 additions and 5 deletions

View File

@ -21,6 +21,12 @@
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- AYCODE_NATIVEAOT compile symbol — defined whenever PublishAot=true is in effect.
Used by Program.cs to #if-out MessagePack benchmark sites: MessagePack v3 has no AOT-compatible
resolver in this project's setup (DynamicGenericResolver fails on trimmed ListFormatter<T>).
This is a benchmark-project-local workaround and never ships as NuGet — directives are safe here. -->
<DefineConstants>$(DefineConstants);AYCODE_NATIVEAOT</DefineConstants>
<!-- Először tegyük zsongva: nyeljük le a trim warning-okat hogy buildelni tudjon. -->
<!-- Ezt később vissza lehet kapcsolni szigorúra. -->
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>

View File

@ -4,8 +4,10 @@ using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.Serialization; // DrainFromAsync extension (test-only, used by benchmark)
using AyCode.Core.Tests.TestModels;
using MemoryPack;
#if !AYCODE_NATIVEAOT
using MessagePack;
using MessagePack.Resolvers;
#endif
using Microsoft.Extensions.Options;
using System.Buffers;
using System.Diagnostics;
@ -52,7 +54,9 @@ public static class Program
// Engine identifiers (used in Engine column + comparison logic)
private const string EngineAcBinary = "AcBinary";
private const string EngineMemoryPack = "MemoryPack";
#if !AYCODE_NATIVEAOT
private const string EngineMessagePack = "MessagePack";
#endif
private const string EngineSystemTextJson = "System.Text.Json";
// IO mode identifiers (used in IO column + comparison logic)
@ -557,9 +561,9 @@ public static class Program
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, "FastMode"),
// Fastest Byte[] — Runtime path (UseGeneratedCode=false). Same wire/options, no source-generated dispatch.
// Always paired with the SGen variant so every layer can compare the SGen speed-up apples-to-apples.
// COMMENTED: Reflection.Emit-based dispatch crashes under NativeAOT (PlatformNotSupportedException).
// Re-enable for JIT-mode benchmarks where SGen-vs-Runtime delta matters.
//new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, "FastMode"),
// NativeAOT-safe: AcSerializerCommon.Create*Getter/Setter falls back to reflection-based delegates
// when RuntimeFeature.IsDynamicCodeSupported is false (slower but works under AOT publish).
new AcBinaryBenchmark(testData.Order, binaryFastModeNoSgenOption, "FastMode"),
// Default preset Byte[] — RefHandling=OnlyId (deduplicates IId-shared references on the wire) +
// UseStringInterning=All (deduplicates repeated strings). Showcases the Default preset's wire-size
// and CPU trade-off vs FastMode on the ~20% IId-ref / repeated-string test data.
@ -602,7 +606,12 @@ public static class Program
// ============================================================
// MessagePack — for legacy comparison
// ============================================================
#if !AYCODE_NATIVEAOT
// MessagePack v3's DynamicGenericResolver uses Activator.CreateInstance on trimmed
// ListFormatter<T> et al. — fails under NativeAOT publish with "No parameterless constructor".
// Excluded from the AOT build; available for regular JIT runs only.
new MessagePackBenchmark(testData.Order, "ContractBased"),
#endif
// System.Text.Json (commented — JSON serializer for reference; not in active suite)
//new SystemTextJsonBenchmark(testData.Order, "Default")
@ -1031,6 +1040,11 @@ public static class Program
}
}
#if !AYCODE_NATIVEAOT
// MessagePack benchmark — excluded from NativeAOT build because v3's StandardResolver falls back
// to DynamicGenericResolver for closed-generic types (List<TestOrderItem> et al.), which uses
// Activator.CreateInstance on formatter types the AOT trimmer drops → MissingMethodException at runtime.
// Available for regular JIT runs (`dotnet run`) only.
private sealed class MessagePackBenchmark : ISerializerBenchmark
{
private readonly TestOrder _order;
@ -1083,6 +1097,7 @@ public static class Program
return DeepEqualsViaJson(_order, roundTripped);
}
}
#endif
/// <summary>
/// Benchmarks AcBinary via the IBufferWriter overload with a pre-allocated, reused ArrayBufferWriter.

View File

@ -465,6 +465,12 @@ public static class AcSerializerCommon
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
{
// NativeAOT (and other no-dynamic-code targets): fall back to plain reflection.
// The returned delegate is cached per-property by the caller, so the indirection cost is paid
// only at cache-population time — hot path is a direct delegate invocation either way.
if (!RuntimeFeature.IsDynamicCodeSupported)
return prop.GetValue;
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
@ -478,6 +484,9 @@ public static class AcSerializerCommon
/// </summary>
public static Action<object, object?> CreateCompiledSetter(Type declaringType, PropertyInfo prop)
{
if (!RuntimeFeature.IsDynamicCodeSupported)
return prop.SetValue;
var targetParam = LExpression.Parameter(typeof(object), "target");
var valueParam = LExpression.Parameter(typeof(object), "value");
@ -524,6 +533,9 @@ public static class AcSerializerCommon
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null);
if (ctor == null) return null;
if (!RuntimeFeature.IsDynamicCodeSupported)
return () => ctor.Invoke(null);
var newExpr = LExpression.New(ctor);
var convert = LExpression.Convert(newExpr, typeof(object));
return LExpression.Lambda<Func<object>>(convert).Compile();
@ -534,15 +546,18 @@ public static class AcSerializerCommon
/// </summary>
public static Func<object, TProperty> CreateTypedGetter<TProperty>(Type declaringType, PropertyInfo prop)
{
if (!RuntimeFeature.IsDynamicCodeSupported)
return obj => (TProperty)prop.GetValue(obj)!;
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
// Only convert if property type differs from TProperty (avoids unnecessary boxing)
Expression resultExpr = prop.PropertyType == typeof(TProperty)
? propAccess
: LExpression.Convert(propAccess, typeof(TProperty));
return LExpression.Lambda<Func<object, TProperty>>(resultExpr, objParam).Compile();
}
@ -551,6 +566,9 @@ public static class AcSerializerCommon
/// </summary>
public static Func<object, int> CreateEnumGetter(Type declaringType, PropertyInfo prop)
{
if (!RuntimeFeature.IsDynamicCodeSupported)
return obj => Convert.ToInt32(prop.GetValue(obj));
var objParam = LExpression.Parameter(typeof(object), "obj");
var castExpr = LExpression.Convert(objParam, declaringType);
var propAccess = LExpression.Property(castExpr, prop);
@ -563,6 +581,9 @@ public static class AcSerializerCommon
/// </summary>
public static Action<object, TProperty> CreateTypedSetter<TProperty>(Type declaringType, PropertyInfo prop)
{
if (!RuntimeFeature.IsDynamicCodeSupported)
return (obj, value) => prop.SetValue(obj, value);
var objParam = LExpression.Parameter(typeof(object), "obj");
var valueParam = LExpression.Parameter(typeof(TProperty), "value");
var castExpr = LExpression.Convert(objParam, declaringType);
@ -576,6 +597,12 @@ public static class AcSerializerCommon
/// </summary>
public static Action<object, int> CreateEnumSetter(Type declaringType, PropertyInfo prop)
{
if (!RuntimeFeature.IsDynamicCodeSupported)
{
var enumType = prop.PropertyType;
return (obj, value) => prop.SetValue(obj, Enum.ToObject(enumType, value));
}
var objParam = LExpression.Parameter(typeof(object), "obj");
var valueParam = LExpression.Parameter(typeof(int), "value");
var castExpr = LExpression.Convert(objParam, declaringType);