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.