From 3ab6b48143f51a0cd4fde2e3f9c46bc651776416 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:08:35 +0900 Subject: [PATCH] bench: compare .NET 10 JIT and NativeAOT compilation and execution --- sandbox/Benchmark.NativeAot/.gitignore | 3 + .../Benchmark.NativeAot.csproj | 15 ++ sandbox/Benchmark.NativeAot/Program.cs | 34 +++++ sandbox/Benchmark.NativeAot/README.md | 42 ++++++ .../Benchmark.NativeAot/RuntimeComparison.cs | 140 ++++++++++++++++++ sandbox/Benchmark.NativeAot/global.json | 6 + sandbox/Benchmark/README.md | 3 + 7 files changed, 243 insertions(+) create mode 100644 sandbox/Benchmark.NativeAot/.gitignore create mode 100644 sandbox/Benchmark.NativeAot/Benchmark.NativeAot.csproj create mode 100644 sandbox/Benchmark.NativeAot/Program.cs create mode 100644 sandbox/Benchmark.NativeAot/README.md create mode 100644 sandbox/Benchmark.NativeAot/RuntimeComparison.cs create mode 100644 sandbox/Benchmark.NativeAot/global.json diff --git a/sandbox/Benchmark.NativeAot/.gitignore b/sandbox/Benchmark.NativeAot/.gitignore new file mode 100644 index 00000000..502a49c3 --- /dev/null +++ b/sandbox/Benchmark.NativeAot/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +BenchmarkDotNet.Artifacts/ diff --git a/sandbox/Benchmark.NativeAot/Benchmark.NativeAot.csproj b/sandbox/Benchmark.NativeAot/Benchmark.NativeAot.csproj new file mode 100644 index 00000000..80c766fb --- /dev/null +++ b/sandbox/Benchmark.NativeAot/Benchmark.NativeAot.csproj @@ -0,0 +1,15 @@ + + + Exe + net10.0 + enable + enable + + + + + + + + + diff --git a/sandbox/Benchmark.NativeAot/Program.cs b/sandbox/Benchmark.NativeAot/Program.cs new file mode 100644 index 00000000..48dbb969 --- /dev/null +++ b/sandbox/Benchmark.NativeAot/Program.cs @@ -0,0 +1,34 @@ +using BenchmarkDotNet.Running; + +if (args.Contains("--verify")) +{ + foreach ( + var workload in ComparisonWorkloads + .Cases.Concat(ComparisonWorkloads.CompilationCases) + .DistinctBy(c => c.Name) + ) + RuntimeVerification.Verify(workload); + return; +} + +BenchmarkSwitcher + .FromTypes([typeof(RuntimeExecutionComparison), typeof(RuntimeCompilationComparison)]) + .Run(args); + +static class RuntimeVerification +{ + public static void Verify(ComparisonCase workload) + { + foreach (var bytes in new[] { false, true }) + { + using var runner = new LuaCSharpComparison(workload.Source, bytes); + for (var i = 0; i < 2; i++) + ComparisonWorkloads.Check( + workload, + bytes ? "Lua-CSharp bytes" : "Lua-CSharp", + runner.Run() + ); + } + Console.WriteLine($"PASS {workload.Name}: {workload.Expected:R}"); + } +} diff --git a/sandbox/Benchmark.NativeAot/README.md b/sandbox/Benchmark.NativeAot/README.md new file mode 100644 index 00000000..f24e3393 --- /dev/null +++ b/sandbox/Benchmark.NativeAot/README.md @@ -0,0 +1,42 @@ +# .NET 10 JIT and NativeAOT benchmarks + +Compare Lua-CSharp source compilation and bytecode execution under .NET 10 JIT +and NativeAOT, using the sources and checksum checks from `../Benchmark`. +NativeAOT compiles the C# interpreter; Lua source is still compiled to Lua +bytecode and interpreted. + +Run from this directory: + +```powershell +dotnet restore Benchmark.NativeAot.csproj +dotnet build Benchmark.NativeAot.csproj -c Release --no-restore +dotnet bin/Release/net10.0/Benchmark.NativeAot.dll --verify +dotnet bin/Release/net10.0/Benchmark.NativeAot.dll --filter '*RuntimeExecutionComparison*numeric-loop*' --exporters json +dotnet bin/Release/net10.0/Benchmark.NativeAot.dll --filter '*RuntimeCompilationComparison*numeric-loop*' --exporters json +``` + +Use a case filter before selecting a whole suite. `--list flat` lists the +benchmarks; no arguments opens BenchmarkDotNet's selector. `--artifacts ` +keeps outputs separate and `--keepFiles` retains generated projects/executables. + +The BenchmarkDotNet NativeAOT toolchain publishes a native benchmark process. +Install the platform's NativeAOT build prerequisites (Visual C++ build tools +and a Windows SDK on Windows). The native job pins compiler/runtime **10.0.14**, +uses speed optimization and the current CPU's native instruction set. Record +the resolved JIT runtime version too. These native executables target the build +machine's CPU capabilities. + +`--verify` checks all 15 distinct workloads in the managed host, twice in each +string mode. Each benchmark's setup also runs its checksum checks inside the +actual JIT or NativeAOT benchmark process before timing. Timed execution uses +UTF-16 mode with ASCII inputs, retained state and a compiled chunk. Compilation +has six source cases, a fresh state outside each batch of 64 loads, and no Lua +execution during measurement. Both jobs use three launches. Compilation warms +for two seconds, then measures 20 batches after 20 warmups; execution measures +10 iterations after five warmups with a 250 ms target iteration time. + +Run timing jobs serially. JIT uses runtime defaults; NativeAOT has no tiered JIT +or dynamic PGO. Compare the same sources and keep these profiles distinct from +cross-engine results. MemoryDiagnoser reports managed allocation; generated +executable sizes also include BenchmarkDotNet and metadata, so neither is a +minimal embedding application size or total native-Lua memory measurement. diff --git a/sandbox/Benchmark.NativeAot/RuntimeComparison.cs b/sandbox/Benchmark.NativeAot/RuntimeComparison.cs new file mode 100644 index 00000000..8c7d1d1d --- /dev/null +++ b/sandbox/Benchmark.NativeAot/RuntimeComparison.cs @@ -0,0 +1,140 @@ +using System.Diagnostics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.NativeAot; +using Lua; +using Lua.Runtime; +using Lua.Standard; +using Perfolizer.Horology; + +static class RuntimeJobs +{ + public static Job NativeAot(Job job) => + job.WithRuntime(NativeAotRuntime.Net10_0) + .WithArguments([ + new MsBuildArgument("/p:RestoreSources=https://api.nuget.org/v3/index.json"), + ]) + .WithToolchain( + NativeAotToolchain + .CreateBuilder() + .UseNuGet("10.0.14", "https://api.nuget.org/v3/index.json") + .IlcInstructionSet("native") + .TargetFrameworkMoniker("net10.0") + .RuntimeFrameworkVersion("10.0.14") + .UseNuGetClearTag(true) + .DisplayName("NativeAOT 10.0.14") + .ToToolchain() + ) + .WithId("NET10-NativeAOT"); +} + +public sealed class RuntimeExecutionConfig : ManualConfig +{ + public RuntimeExecutionConfig() + { + AddDiagnoser(MemoryDiagnoser.Default); + var job = Job + .ShortRun.WithLaunchCount(3) + .WithWarmupCount(5) + .WithIterationCount(10) + .WithIterationTime(TimeInterval.FromMilliseconds(250)); + AddJob(job.WithRuntime(CoreRuntime.Core10_0).WithId("NET10-JIT").AsBaseline()); + AddJob(RuntimeJobs.NativeAot(job)); + } +} + +public sealed class RuntimeCompilationConfig : ManualConfig +{ + public RuntimeCompilationConfig() + { + AddDiagnoser(MemoryDiagnoser.Default); + var job = Job + .ShortRun.WithLaunchCount(3) + .WithWarmupCount(20) + .WithIterationCount(20) + .WithInvocationCount(1) + .WithUnrollFactor(1); + AddJob(job.WithRuntime(CoreRuntime.Core10_0).WithId("NET10-JIT").AsBaseline()); + AddJob(RuntimeJobs.NativeAot(job)); + } +} + +[Config(typeof(RuntimeExecutionConfig))] +[BenchmarkCategory("Execution")] +public class RuntimeExecutionComparison +{ + [ParamsSource(nameof(Cases))] + public string Case { get; set; } = "numeric-loop"; + public static IEnumerable Cases => ComparisonWorkloads.Cases.Select(c => c.Name); + LuaCSharpComparison runner = default!; + + [GlobalSetup] + public void Setup() + { + var workload = ComparisonWorkloads.Find(Case); + RuntimeVerification.Verify(workload); + runner = new(workload.Source); + } + + [Benchmark] + public double Execute() => runner.Run(); + + [GlobalCleanup] + public void Cleanup() => runner.Dispose(); +} + +[Config(typeof(RuntimeCompilationConfig))] +[BenchmarkCategory("Compilation")] +public class RuntimeCompilationComparison +{ + [ParamsSource(nameof(Cases))] + public string Case { get; set; } = "numeric-loop"; + public static IEnumerable Cases => + ComparisonWorkloads.CompilationCases.Select(c => c.Name); + string source = default!; + LuaState state = default!; + const int BatchSize = 64; + + [GlobalSetup] + public void Setup() + { + var workload = ComparisonWorkloads.FindCompilation(Case); + RuntimeVerification.Verify(workload); + source = workload.Source; + var timer = Stopwatch.StartNew(); + do + { + ResetState(); + try + { + Compile(); + } + finally + { + Cleanup(); + } + } while (timer.ElapsedMilliseconds < 2000); + } + + [IterationSetup] + public void ResetState() + { + state = LuaState.Create(); + state.OpenStandardLibraries(); + } + + [Benchmark(OperationsPerInvoke = BatchSize)] + public LuaClosure Compile() + { + LuaClosure result = default!; + for (var i = 0; i < BatchSize; i++) + result = state.Load(source, "comparison"); + return result; + } + + [IterationCleanup] + public void Cleanup() => state.Dispose(); +} diff --git a/sandbox/Benchmark.NativeAot/global.json b/sandbox/Benchmark.NativeAot/global.json new file mode 100644 index 00000000..512142d2 --- /dev/null +++ b/sandbox/Benchmark.NativeAot/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/sandbox/Benchmark/README.md b/sandbox/Benchmark/README.md index 6d9a8ac6..d45b9fab 100644 --- a/sandbox/Benchmark/README.md +++ b/sandbox/Benchmark/README.md @@ -69,3 +69,6 @@ single overall ranking without application-specific workload weights. The legacy suites include file I/O, source execution and focused API tests with different boundaries. Their results should not be mixed with this suite. + +For Lua-CSharp JIT versus NativeAOT compilation and execution, use the +[NativeAOT benchmarks](../Benchmark.NativeAot/README.md), which share these sources.