Compile without reflection, and gate on a trimmed and NativeAOT run (#363, #746) - #1016
Merged
Conversation
BenchLinqCompilation and CacheCompiledFunc had no way to be run: the first was a commented-out BenchmarkRunner.Run at the bottom of Main, the second had no arm in the switch at all. A benchmark nobody can invoke measures nothing, and these two are the only coverage of the Linq compilation path apart from CommonFunctionsInterVersion's CompileEasy/CompileHard. Both now take an argument like the others, and both report Allocated as well as Mean -- #746's item 80 asks for time and allocation, and allocation is the axis on which the compilation path actually moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
…363, #746) The Linq compilation path named four kinds of member at run time, and each is invisible to the trimmer and to the NativeAOT compiler: - typeof(MathAllMethods).GetMethod(name, types) for every mathematical function. Both tools answer a run-time name by keeping every public method of the class, and the run paid an overload resolution per node. - expr.Type.GetMethod("IsNaN") for the NaN test, which the trimmer had no reason to keep. IL2075, and under NativeAOT a null that reached AngouriBugException and took the process down on the first double -> long? conversion. - typeof(Nullable<>).MakeGenericType(t), which NativeAOT cannot promise. IL3050. - the operators and conversions Expression.Add and Expression.Convert find by reflecting over the operand or target type. Complex and BigInteger define theirs as methods, and NativeAOT kept no metadata for them: "x + 1" over a complex argument threw "the binary operator Add is not defined for the types 'System.Numerics.Complex' and 'System.Numerics.Complex'", and "double -> Complex" threw "no coercion operator is defined between types". All four are now tables of members named at compile time. MathAllMethods.Definitions is generated from the same lists as the methods it dispatches to, so the two cannot drift apart, and every entry is an expression tree because the compiler emits an ldtoken for the call inside one -- which is how C# names a method without a string, and what roots it for the trimmer. Sources/Tests/AotSmokeTest is the gate. It parses, simplifies, solves, differentiates, integrates, takes a limit and compiles twenty-two expressions, asserts values rather than printed forms, and returns non-zero on any mismatch. It publishes trimmed and NativeAOT with TrimmerSingleWarn=false and the trimmer's and ILC's own warnings-as-errors switches, and the workflow runs the published binary: everything that goes wrong here goes wrong silently at publish time, so publishing proves nothing and only running does. Measured on linux-x64, .NET 10.0.10: both publishes report zero IL2xxx/IL3xxx, and the 44 assertions give byte-identical output under the JIT, the trimmed binary and the NativeAOT binary. Before this, the NativeAOT binary failed six of them and aborted on the seventh. The kernel is marked IsAotCompatible for net8.0 and later as a result, which is also a promise to consumers and is recorded in BREAKING-CHANGES.md. CompileEasy allocates 16,377 B -> 11,122 B and CompileHard 38,090 B -> 20,433 B, with every other row of CommonFunctionsInterVersion unchanged to the byte; BenchLinqCompilation and CacheCompiledFunc, which run compiled delegates rather than compiling them, are unchanged within noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011emtnRT6EWTrxXNtqDVK3e
Collaborator
Author
|
The caveat in the description is closed: the workflow has now been observed firing, and it passes on every leg.
All three jobs Two things that were argued rather than measured before are now measured:
The trimmed and NativeAOT publishes are both under |
This was referenced Aug 23, 2026
…er scopes Three files collided, all three additively. BREAKING-CHANGES.md and Sources/.editorconfig each had two branches appending to the same place, so every entry from both sides survives; AGENTS.md and DotnetBenchmark/Program.cs merged without help.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#746 item 79 asks for a trimming and NativeAOT smoke test "and fail the build if the kernel path breaks or warns"; item 57 / #363 asks for AOT-supported Linq compilation. Neither existed: there was no
PublishTrimmed,IsTrimmable,IsAotCompatibleorEnableTrimAnalyzeranywhere in the tree.The premise I was given was wrong, and that is the main finding
The obvious suspect —
CompilationProtocol.cs'stypeof(MathAllMethods).GetMethod(name, types), a run-time lookup by string — emits no IL warning and broke neither build.typeof(X)is a known type, so ILLink and ILC answer an unknown method name by keeping every public method of the class. Removing it is still right (it forces that retention, and costs an overload resolution per node) but it was not the defect.The real defects, in increasing order of how hard they were to see:
expr.Type.GetMethod("IsNaN")— IL2075, the only warning either tool produced from the whole kernel.typeof(Nullable<>).MakeGenericType(t)— IL3050, NativeAOT only.Expression.AddandExpression.ConvertfindComplex.op_AdditionandComplex.op_Implicitby reflecting over the operand type. ILC kept no metadata for them. Invisible at publish time; they appeared only when the binary ran.That last pair is why "publish it" is not the acceptance criterion and "run it" is. Baseline NativeAOT run, before the fix:
What is here
Sources/Tests/AotSmokeTest/— 44 value assertions over parse,Simplify,Solve,Differentiate,Integrate,LimitandCompile(22 compiled expressions covering everyMathAllMethodsoverload group, both arities,double/long/Complex/nullable returns, booleans, piecewise). Non-zero exit on any mismatch. It setsPublishTrimmed+TrimMode=fulland all three warnings-as-errors switches —TreatWarningsAsErrors,ILLinkTreatWarningsAsErrors,IlcTreatWarningsAsErrors, none of which implies another..github/workflows/TrimmingAndAot.yml— ubuntu/windows/macos × {trimmed publish, NativeAOT publish}, and it runs both binaries.Kernel de-reflection —
CompilationProtocol.csandMathAllMethods.tt: four member tables named at compile time through expression trees (ldtoken), replacing the string lookup,GetMethod("IsNaN"),MakeGenericType, and the operator/conversion lookups.AngouriMath.csprojsetsIsAotCompatiblefor net8.0+.Verified, independently of the agent that wrote it
Published and run from a clean
bin/objon this machine:dotnet publish -c Release -r linux-x64 --self-containedpassed: 44, failed: 0, exit 0dotnet publish -c Release -r linux-x64 -p:AngouriMathPublishAot=truefileconfirmsELF 64-bit LSB pie executable); runs →passed: 44, failed: 0, exit 0clang, lld and zlib1g-dev were installed so the AOT half is genuinely measured rather than skipped.
One csproj detail worth knowing:
-p:PublishAot=trueon the command line propagates toProjectReferences and fails on the kernel'snetstandard2.0target with NETSDK1207 before the sample is looked at. The project opts in through a privateAngouriMathPublishAotproperty instead.Measured
Compile<Complex, Complex>is unchanged. 26 expressions printed atG17: before-JIT vs after-JITdiffs identical, and after-trimmed and after-NativeAOT are identical to both.Suite: 7533 passed, 0 failed, 14 skipped — the baseline exactly. F# wrapper 134/0.
Benchmarks, same machine back to back:
The whole "after" run is ~10% faster on every row including untouched ones, so the timings are machine state and are not claimed. Allocation is the clean signal: it moves only on the two compile rows and is identical to the byte everywhere else (
SimplifyEasy128,100 → 128,099;ParseHard3,522,409 → 3,522,409). So this is a real allocation win on compilation and a no-op elsewhere.Also verified that
AngouriMath.CPP.Exportingstill publishes NativeAOT with zero IL warnings and 72 exported symbols — which matters, because marking the kernelIsTrimmablechanges what ILC removes from it in that build.Two smaller things in the same branch
MathAllMethods.ttdid not emit the copyright header its generated file carries, so regenerating dropped it and IDE0073 (a build error here) would have failed the build. The unmodified template was regenerated and diffed first — header only — before anything else changed.BenchmarkRunner.Run, the other had no arm in the switch. Both wired, both now reportAllocated. That is commit 2 and is independently revertable.For a maintainer
IsAotCompatibleon the kernel is a promise to consumers. A trimmer now removes code from insideAngouriMath.dllwhere before it kept the assembly whole. True today and gated by this workflow, but it is a deliberate commitment rather than a warning being silenced.paths:filter and its trigger block is byte-identical toCPPTest.yml's, which fires on every push and PR. Demonstration by identical construction plus evidence, not proof of the file itself; the run on this PR is the proof.BREAKING-CHANGES.mdgains a## Unreleased — since 2.3.0heading, which has no precedent in the file and will conflict with any concurrent branch adding one. Print the brackets an operator that is not associative has #1009 adds the same heading.The
CPPBuild.ymlpath-filter fix that was found alongside this is split out as #1015, since it has nothing to do with AOT.