Skip to content

Commit 7f583be

Browse files
committed
Align AOT CodeGen.
1 parent 7bd81aa commit 7f583be

File tree

11 files changed

+83
-68
lines changed

11 files changed

+83
-68
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace BenchmarkDotNet.Code;
2+
3+
/// <summary>
4+
/// Specifies how to generate the code that calls the benchmark's Run method.
5+
/// </summary>
6+
public enum CodeGenBenchmarkRunCallType
7+
{
8+
/// <summary>
9+
/// Use reflection to call the benchmark's Run method indirectly.
10+
/// </summary>
11+
/// <remarks>
12+
/// This is to avoid strong dependency Main-to-Runnable
13+
/// which could cause the jitting/assembly loading to happen before we do anything.
14+
/// We have some jitting diagnosers and we want them to catch all the informations.
15+
/// </remarks>
16+
Reflection,
17+
/// <summary>
18+
/// Uses a switch to select the benchmark to call Run directly.
19+
/// </summary>
20+
/// <remarks>
21+
/// This is for AOT runtimes where reflection may not exist or the benchmark types
22+
/// could be trimmed out when they are not directly referenced.
23+
/// </remarks>
24+
Direct
25+
}

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@ namespace BenchmarkDotNet.Code
2121
{
2222
internal static class CodeGenerator
2323
{
24-
internal static string Generate(BuildPartition buildPartition)
24+
internal static string Generate(BuildPartition buildPartition, CodeGenBenchmarkRunCallType benchmarkRunCallType)
2525
{
2626
(bool useShadowCopy, string shadowCopyFolderPath) = GetShadowCopySettings();
2727

2828
var benchmarksCode = new List<string>(buildPartition.Benchmarks.Length);
2929

30-
var extraDefines = new List<string>();
31-
3230
foreach (var buildInfo in buildPartition.Benchmarks)
3331
{
3432
var benchmark = buildInfo.BenchmarkCase;
@@ -64,19 +62,12 @@ internal static string Generate(BuildPartition buildPartition)
6462
benchmarksCode.Add(benchmarkTypeCode);
6563
}
6664

67-
if (buildPartition.IsNativeAot)
68-
extraDefines.Add("#define NATIVEAOT");
69-
else if (buildPartition.IsNetFramework)
70-
extraDefines.Add("#define NETFRAMEWORK");
71-
else if (buildPartition.IsWasm)
72-
extraDefines.Add("#define WASM");
73-
7465
string benchmarkProgramContent = new SmartStringBuilder(ResourceHelper.LoadTemplate("BenchmarkProgram.txt"))
7566
.Replace("$ShadowCopyDefines$", useShadowCopy ? "#define SHADOWCOPY" : null).Replace("$ShadowCopyFolderPath$", shadowCopyFolderPath)
76-
.Replace("$ExtraDefines$", string.Join(Environment.NewLine, extraDefines))
67+
.Replace("$ExtraDefines$", buildPartition.IsNetFramework ? "#define NETFRAMEWORK" : string.Empty)
7768
.Replace("$DerivedTypes$", string.Join(Environment.NewLine, benchmarksCode))
7869
.Replace("$ExtraAttribute$", GetExtraAttributes(buildPartition.RepresentativeBenchmarkCase.Descriptor))
79-
.Replace("$NativeAotSwitch$", GetNativeAotSwitch(buildPartition))
70+
.Replace("$BenchmarkRunCall$", GetBenchmarkRunCall(buildPartition, benchmarkRunCallType))
8071
.ToString();
8172

8273
return benchmarkProgramContent;
@@ -251,14 +242,20 @@ private static string GetParameterModifier(ParameterInfo parameterInfo)
251242
return "ref";
252243
}
253244

254-
/// <summary>
255-
/// for NativeAOT we can't use reflection to load type and run a method, so we simply generate a switch for all types..
256-
/// </summary>
257-
private static string GetNativeAotSwitch(BuildPartition buildPartition)
245+
private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGenBenchmarkRunCallType runCallType)
258246
{
259-
if (!buildPartition.IsNativeAot)
260-
return default;
247+
if (runCallType == CodeGenBenchmarkRunCallType.Reflection)
248+
{
249+
// Use reflection to call benchmark's Run method indirectly.
250+
return """
251+
typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly
252+
.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}")
253+
.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static)
254+
.Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode });
255+
""";
256+
}
261257

258+
// Generate a switch to call benchmark's Run method directly.
262259
var @switch = new StringBuilder(buildPartition.Benchmarks.Length * 30);
263260
@switch.AppendLine("switch (id) {");
264261

src/BenchmarkDotNet/Running/BuildPartition.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using BenchmarkDotNet.Toolchains;
1313
using BenchmarkDotNet.Toolchains.CsProj;
1414
using BenchmarkDotNet.Toolchains.DotNetCli;
15-
using BenchmarkDotNet.Toolchains.MonoWasm;
1615
using BenchmarkDotNet.Toolchains.Roslyn;
1716
using JetBrains.Annotations;
1817

@@ -57,9 +56,6 @@ public BuildPartition(BenchmarkBuildInfo[] benchmarks, IResolver resolver)
5756

5857
public bool IsNativeAot => RepresentativeBenchmarkCase.Job.IsNativeAOT();
5958

60-
public bool IsWasm => Runtime is WasmRuntime // given job can have Wasm toolchain set, but Runtime == default ;)
61-
|| (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && toolchain is WasmToolchain);
62-
6359
public bool IsNetFramework => Runtime is ClrRuntime
6460
|| (RepresentativeBenchmarkCase.Job.Infrastructure.TryGetToolchain(out var toolchain) && (toolchain is RoslynToolchain || toolchain is CsProjClassicNetToolchain));
6561

src/BenchmarkDotNet/Templates/BenchmarkProgram.txt

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ $ExtraDefines$
44
// this file must not be importing any namespaces
55
// we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778
66

7+
#pragma warning disable CS0436 // Type conflicts with imported type
78
#if !NET7_0_OR_GREATER
89
namespace System.Diagnostics.CodeAnalysis
910
{
@@ -18,8 +19,9 @@ namespace System.Diagnostics.CodeAnalysis
1819
}
1920
}
2021
#endif
22+
#pragma warning restore CS0436 // Type conflicts with imported type
2123

22-
// the namespace name must be in sync with WindowsDisassembler.BuildArguments
24+
// the namespace name must be in sync with DisassemblyDiagnoser.BuildClrMdArgs
2325
namespace BenchmarkDotNet.Autogenerated
2426
{
2527
public class UniqueProgramName // we need different name than typical "Program" to avoid problems with referencing "Program" types from benchmarked code, #691
@@ -38,7 +40,7 @@ namespace BenchmarkDotNet.Autogenerated
3840

3941
private static global::System.Int32 AfterAssemblyLoadingAttached(global::System.String[] args)
4042
{
41-
global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
43+
global::BenchmarkDotNet.Engines.IHost host; // this variable name is used by CodeGenerator.GetBenchmarkRunCall, do NOT change it
4244
if (global::BenchmarkDotNet.Engines.AnonymousPipesHost.TryGetFileHandles(args, out global::System.String writeHandle, out global::System.String readHandle))
4345
host = new global::BenchmarkDotNet.Engines.AnonymousPipesHost(writeHandle, readHandle);
4446
else
@@ -50,27 +52,18 @@ namespace BenchmarkDotNet.Autogenerated
5052

5153
try
5254
{
53-
// we are not using Runnable here in any direct way in order to avoid strong dependency Main<=>Runnable
54-
// which could cause the jitting/assembly loading to happen before we do anything
55-
// we have some jitting diagnosers and we want them to catch all the informations!!
56-
57-
// this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
55+
// These variable names are used by CodeGenerator.GetBenchmarkRunCall, do NOT change them!
5856
global::System.String benchmarkName = global::System.Linq.Enumerable.FirstOrDefault(global::System.Linq.Enumerable.Skip(global::System.Linq.Enumerable.SkipWhile(args, arg => arg != "--benchmarkName"), 1)) ?? "not provided";
5957
global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode = (global::BenchmarkDotNet.Diagnosers.RunMode) global::System.Int32.Parse(global::System.Linq.Enumerable.FirstOrDefault(global::System.Linq.Enumerable.Skip(global::System.Linq.Enumerable.SkipWhile(args, arg => arg != "--diagnoserRunMode"), 1)) ?? "0");
6058
global::System.Int32 id = args.Length > 0
61-
? global::System.Int32.Parse(args[args.Length - 1]) // this variable name is used by CodeGenerator.GetCoreRtSwitch, do NOT change it
59+
? global::System.Int32.Parse(args[args.Length - 1])
6260
: 0; // used when re-using generated exe without BDN (typically to repro a bug)
6361

6462
if (args.Length == 0)
6563
{
6664
host.WriteLine("You have not specified benchmark id (an integer) so the first benchmark will be executed.");
6765
}
68-
#if NATIVEAOT
69-
$NativeAotSwitch$
70-
#else
71-
global::System.Type type = typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly.GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}");
72-
type.GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static).Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode });
73-
#endif
66+
$BenchmarkRunCall$
7467
return 0;
7568
}
7669
catch (global::System.Exception oom) when (oom is global::System.OutOfMemoryException || oom is global::System.Reflection.TargetInvocationException reflection && reflection.InnerException is global::System.OutOfMemoryException)

src/BenchmarkDotNet/Templates/WasmLinkerDescription.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@
22
<assembly fullname="System.Private.CoreLib">
33
<type fullname="System.IntPtr" />
44
</assembly>
5-
<assembly fullname="$PROGRAMNAME$" preserve="all" />
65
</linker>

src/BenchmarkDotNet/Toolchains/GeneratorBase.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using BenchmarkDotNet.Detectors;
66
using BenchmarkDotNet.Extensions;
77
using BenchmarkDotNet.Loggers;
8-
using BenchmarkDotNet.Portability;
98
using BenchmarkDotNet.Running;
109
using BenchmarkDotNet.Toolchains.Results;
1110
using JetBrains.Annotations;
@@ -16,6 +15,9 @@ namespace BenchmarkDotNet.Toolchains
1615
[PublicAPI]
1716
public abstract class GeneratorBase : IGenerator
1817
{
18+
/// <inheritdoc cref="CodeGenBenchmarkRunCallType"/>
19+
protected CodeGenBenchmarkRunCallType BenchmarkRunCallType { get; init; }
20+
1921
public GenerateResult GenerateProject(BuildPartition buildPartition, ILogger logger, string rootArtifactsFolderPath)
2022
{
2123
var artifactsPaths = ArtifactsPaths.Empty;
@@ -119,7 +121,7 @@ [PublicAPI] protected virtual void GenerateAppConfig(BuildPartition buildPartiti
119121
/// <remarks>You most probably do NOT need to override this method!!</remarks>
120122
/// </summary>
121123
[PublicAPI] protected virtual void GenerateCode(BuildPartition buildPartition, ArtifactsPaths artifactsPaths)
122-
=> File.WriteAllText(artifactsPaths.ProgramCodePath, CodeGenerator.Generate(buildPartition));
124+
=> File.WriteAllText(artifactsPaths.ProgramCodePath, CodeGenerator.Generate(buildPartition, BenchmarkRunCallType));
123125

124126
protected virtual string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, $"{programName}{GetExecutableExtension()}");
125127

src/BenchmarkDotNet/Toolchains/MonoAotLLVM/MonoAotLLVMGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public MonoAotLLVMGenerator(string targetFrameworkMoniker, string cliPath, strin
2323
CustomRuntimePack = customRuntimePack;
2424
AotCompilerPath = aotCompilerPath;
2525
AotCompilerMode = aotCompilerMode;
26+
BenchmarkRunCallType = Code.CodeGenBenchmarkRunCallType.Direct;
2627
}
2728

2829
protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)

src/BenchmarkDotNet/Toolchains/MonoWasm/WasmGenerator.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,25 @@ namespace BenchmarkDotNet.Toolchains.MonoWasm
1313
public class WasmGenerator : CsProjGenerator
1414
{
1515
private readonly string CustomRuntimePack;
16-
private readonly bool Aot;
1716
private readonly string MainJS;
1817

1918
public WasmGenerator(string targetFrameworkMoniker, string cliPath, string packagesPath, string customRuntimePack, bool aot)
2019
: base(targetFrameworkMoniker, cliPath, packagesPath, runtimeFrameworkVersion: null)
2120
{
22-
Aot = aot;
2321
CustomRuntimePack = customRuntimePack;
2422
MainJS = (targetFrameworkMoniker == "net5.0" || targetFrameworkMoniker == "net6.0") ? "main.js" : "test-main.js";
23+
BenchmarkRunCallType = aot ? Code.CodeGenBenchmarkRunCallType.Direct : Code.CodeGenBenchmarkRunCallType.Reflection;
2524
}
2625

2726
protected override void GenerateProject(BuildPartition buildPartition, ArtifactsPaths artifactsPaths, ILogger logger)
2827
{
2928
if (((WasmRuntime)buildPartition.Runtime).Aot)
3029
{
3130
GenerateProjectFile(buildPartition, artifactsPaths, aot: true, logger);
32-
GenerateLinkerDescriptionFile(artifactsPaths);
33-
}
34-
else
31+
32+
var linkDescriptionFileName = "WasmLinkerDescription.xml";
33+
File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath), linkDescriptionFileName), ResourceHelper.LoadTemplate(linkDescriptionFileName));
34+
} else
3535
{
3636
GenerateProjectFile(buildPartition, artifactsPaths, aot: false, logger);
3737
}
@@ -66,16 +66,6 @@ protected void GenerateProjectFile(BuildPartition buildPartition, ArtifactsPaths
6666
GatherReferences(buildPartition, artifactsPaths, logger);
6767
}
6868

69-
protected void GenerateLinkerDescriptionFile(ArtifactsPaths artifactsPaths)
70-
{
71-
const string linkDescriptionFileName = "WasmLinkerDescription.xml";
72-
73-
var content = ResourceHelper.LoadTemplate(linkDescriptionFileName)
74-
.Replace("$PROGRAMNAME$", artifactsPaths.ProgramName);
75-
76-
File.WriteAllText(Path.Combine(Path.GetDirectoryName(artifactsPaths.ProjectFilePath)!, linkDescriptionFileName), content);
77-
}
78-
7969
protected override string GetExecutablePath(string binariesDirectoryPath, string programName) => Path.Combine(binariesDirectoryPath, "AppBundle", MainJS);
8070

8171
protected override string GetBinariesDirectoryPath(string buildArtifactsDirectoryPath, string configuration)

src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal Generator(string ilCompilerVersion,
4646
this.ilcGenerateStackTraceData = ilcGenerateStackTraceData;
4747
this.ilcOptimizationPreference = ilcOptimizationPreference;
4848
this.ilcInstructionSet = ilcInstructionSet;
49+
BenchmarkRunCallType = Code.CodeGenBenchmarkRunCallType.Direct;
4950
}
5051

5152
internal readonly IReadOnlyDictionary<string, string> Feeds;

tests/BenchmarkDotNet.IntegrationTests/WasmTests.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using BenchmarkDotNet.Tests.Loggers;
1212
using BenchmarkDotNet.Tests.XUnit;
1313
using BenchmarkDotNet.Toolchains.DotNetCli;
14+
using BenchmarkDotNet.Toolchains.MonoAotLLVM;
1415
using BenchmarkDotNet.Toolchains.MonoWasm;
1516
using Xunit;
1617
using Xunit.Abstractions;
@@ -27,11 +28,11 @@ namespace BenchmarkDotNet.IntegrationTests
2728
/// </summary>
2829
public class WasmTests(ITestOutputHelper output) : BenchmarkTestExecutor(output)
2930
{
30-
private ManualConfig GetConfig()
31+
private ManualConfig GetConfig(MonoAotCompilerMode aotCompilerMode)
3132
{
3233
var dotnetVersion = "net8.0";
3334
var logger = new OutputLogger(Output);
34-
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm");
35+
var netCoreAppSettings = new NetCoreAppSettings(dotnetVersion, null, "Wasm", aotCompilerMode: aotCompilerMode);
3536
var mainJsPath = Path.Combine(AppContext.BaseDirectory, "AppBundle", "test-main.js");
3637

3738
return ManualConfig.CreateEmpty()
@@ -44,20 +45,24 @@ private ManualConfig GetConfig()
4445
.WithOption(ConfigOptions.GenerateMSBuildBinLog, true);
4546
}
4647

47-
[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
48-
public void WasmIsSupported()
48+
[TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
49+
[InlineData(MonoAotCompilerMode.mini)]
50+
[InlineData(MonoAotCompilerMode.wasm)]
51+
public void WasmIsSupported(MonoAotCompilerMode aotCompilerMode)
4952
{
5053
// Test fails on Linux non-x64.
5154
if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64)
5255
{
5356
return;
5457
}
5558

56-
CanExecute<WasmBenchmark>(GetConfig());
59+
CanExecute<WasmBenchmark>(GetConfig(aotCompilerMode));
5760
}
5861

59-
[FactEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
60-
public void WasmSupportsInProcessDiagnosers()
62+
[TheoryEnvSpecific("WASM is only supported on Unix", EnvRequirement.NonWindows)]
63+
[InlineData(MonoAotCompilerMode.mini)]
64+
[InlineData(MonoAotCompilerMode.wasm)]
65+
public void WasmSupportsInProcessDiagnosers(MonoAotCompilerMode aotCompilerMode)
6166
{
6267
// Test fails on Linux non-x64.
6368
if (OsDetector.IsLinux() && RuntimeInformation.GetCurrentPlatform() != Platform.X64)
@@ -68,7 +73,7 @@ public void WasmSupportsInProcessDiagnosers()
6873
try
6974
{
7075
var diagnoser = new MockInProcessDiagnoser1(BenchmarkDotNet.Diagnosers.RunMode.NoOverhead);
71-
var config = GetConfig().AddDiagnoser(diagnoser);
76+
var config = GetConfig(aotCompilerMode).AddDiagnoser(diagnoser);
7277

7378
CanExecute<WasmBenchmark>(config);
7479

0 commit comments

Comments
 (0)