Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/BenchmarkDotNet/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
[assembly: InternalsVisibleTo("BenchmarkDotNet.Diagnostics.dotMemory,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.ManualRunning,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.ManualRunning.MultipleFrameworks,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
[assembly: InternalsVisibleTo("BenchmarkDotNet.IntegrationTests.RuntimeAsync,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
[assembly: InternalsVisibleTo("BenchmarkDotNet.TestAdapter,PublicKey=" + BenchmarkDotNetInfo.PublicKey)]
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Helpers.Reflection.Emit;
using BenchmarkDotNet.Running;
using Perfolizer.Horology;
Expand All @@ -12,11 +14,30 @@ namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation;

partial class RunnableEmitter
{
// TODO: update this to support runtime-async.
private sealed class AsyncCoreEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark, AwaitableInfo awaitableInfo) : AsyncCoreEmitterBase(buildPartition, moduleBuilder, benchmark)
{
private static readonly MethodInfo KeepAliveWithoutBoxingInMethod = typeof(DeadCodeEliminationHelper)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType.IsByRef);

protected override void EmitWorkloadCore()
{
if (IsRuntimeAsync(Descriptor.WorkloadMethod))
{
// A runtime-async workload returns a built-in task type, which WorkloadCore returns too unless the caller type is overridden.
var workloadCoreReturnType = Descriptor.WorkloadMethod.ResolveAttribute<AsyncCallerTypeAttribute>()?.AsyncCallerType
?? Descriptor.WorkloadMethod.ReturnType;
// Roslyn only compiles the built-in task types as runtime-async, any other task-like keeps its state machine.
if (AwaitHelper.IsBuiltInTaskType(workloadCoreReturnType))
{
var runtimeAsyncWorkloadCoreMethod = EmitRuntimeAsyncWorkloadCore(workloadCoreReturnType);
startWorkloadMethod = EmitAsyncSingleCall(StartWorkloadMethodName, typeof(AsyncVoidMethodBuilder), runtimeAsyncWorkloadCoreMethod, SetupCleanupKind.Other);
return;
}
}

var asyncMethodBuilderType = GetWorkloadCoreAsyncMethodBuilderType(Descriptor.WorkloadMethod.ReturnType);
var builderInfo = BeginAsyncStateMachineTypeBuilder(WorkloadCoreMethodName, asyncMethodBuilderType, runnableBuilder);
var (asyncStateMachineTypeBuilder, publicFields, (ilBuilder, endTryLabel, returnLabel, stateLocal, thisLocal, returnDefaultLocal)) = builderInfo;
Expand Down Expand Up @@ -242,13 +263,7 @@ void EmitMoveNextImpl()
// JIT can't elide whatever produced it.
ilBuilder.EmitStloc(resultLocal);
ilBuilder.EmitLdloca(resultLocal);
var keepAliveInMethod = typeof(DeadCodeEliminationHelper)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.First(m => m.Name == nameof(DeadCodeEliminationHelper.KeepAliveWithoutBoxing)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType.IsByRef)
.MakeGenericMethod(resultType);
ilBuilder.Emit(OpCodes.Call, keepAliveInMethod);
ilBuilder.Emit(OpCodes.Call, KeepAliveWithoutBoxingInMethod.MakeGenericMethod(resultType));
}

// --- Benchmark loop: if (--invokeCount >= 0) goto callBenchmarkLabel ---
Expand Down Expand Up @@ -358,5 +373,158 @@ void EmitMoveNextImpl()
} // end handler
}
}

/*
private async Task WorkloadCore()
{
try
{
if (await this.fieldsContainer.workloadValueTaskSource.GetIsComplete())
{
return;
}
while (true)
{
StartedClock startedClock = ClockExtensions.Start(this.fieldsContainer.clock);
while (--this.fieldsContainer.invokeCount >= 0)
{
await base.Workload();
}
if (await this.fieldsContainer.workloadValueTaskSource.SetResultAndGetIsComplete(startedClock.GetElapsed()))
{
return;
}
}
}
catch (Exception e)
{
fieldsContainer.workloadValueTaskSource.SetException(e);
return;
}
}
*/
private MethodInfo EmitRuntimeAsyncWorkloadCore(Type workloadCoreReturnType)
{
var methodBuilder = DefineRuntimeAsyncMethod(WorkloadCoreMethodName, workloadCoreReturnType);
// The generated code copies the workload's builder override, which Roslyn keeps as an attribute without using the builder.
if (!Descriptor.WorkloadMethod.HasAttribute<AsyncCallerTypeAttribute>()
&& Descriptor.WorkloadMethod.GetAsyncMethodBuilderAttribute() is { } builderAttribute
&& builderAttribute.GetType().GetProperty(nameof(AsyncMethodBuilderAttribute.BuilderType), BindingFlags.Public | BindingFlags.Instance)?.GetValue(builderAttribute) is Type builderType)
{
var attributeCtor = typeof(AsyncMethodBuilderAttribute).GetConstructor([typeof(Type)])
?? throw new MissingMemberException(nameof(AsyncMethodBuilderAttribute));
methodBuilder.SetCustomAttribute(new CustomAttributeBuilder(attributeCtor, [builderType]));
}

var ilBuilder = methodBuilder.GetILGenerator();

var returnLocal = workloadCoreReturnType.IsGenericType
? ilBuilder.DeclareLocal(workloadCoreReturnType.GetGenericArguments()[0])
: null;
var startedClockLocal = ilBuilder.DeclareLocal(typeof(StartedClock));
var resultLocal = awaitableInfo.ResultType == typeof(void)
? null
: ilBuilder.DeclareLocal(awaitableInfo.ResultType);
var invokeCountLocal = ilBuilder.DeclareLocal(typeof(long));
var exceptionLocal = ilBuilder.DeclareLocal(typeof(Exception));

var returnLabel = ilBuilder.DefineLabel();
var startClockLabel = ilBuilder.DefineLabel();
var callBenchmarkLabel = ilBuilder.DefineLabel();
var callBenchmarkLoopLabel = ilBuilder.DefineLabel();

ilBuilder.BeginExceptionBlock();
{
// if (await workloadValueTaskSource.GetIsComplete()) return default;
EmitLoadWorkloadValueTaskSource();
ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadValueTaskSource).GetMethod(nameof(WorkloadValueTaskSource.GetIsComplete), BindingFlags.Public | BindingFlags.Instance)!);
EmitRuntimeAsyncAwait(ilBuilder, typeof(ValueTask<bool>));
ilBuilder.Emit(OpCodes.Brfalse, startClockLabel);
ilBuilder.MaybeEmitSetLocalToDefault(returnLocal);
ilBuilder.Emit(OpCodes.Leave, returnLabel);

// startedClock = ClockExtensions.Start(clock);
ilBuilder.MarkLabel(startClockLabel);
ilBuilder.Emit(OpCodes.Ldarg_0);
ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField);
ilBuilder.Emit(OpCodes.Ldfld, clockField);
ilBuilder.Emit(OpCodes.Call, GetStartClockMethod());
ilBuilder.EmitStloc(startedClockLocal);
ilBuilder.Emit(OpCodes.Br, callBenchmarkLoopLabel);

// T result = await base.Workload(); DeadCodeEliminationHelper.KeepAliveWithoutBoxing(in result);
ilBuilder.MarkLabel(callBenchmarkLabel);
if (!Descriptor.WorkloadMethod.IsStatic)
{
ilBuilder.Emit(OpCodes.Ldarg_0);
}
EmitLoadArgFieldsForCall(ilBuilder, null);
ilBuilder.Emit(OpCodes.Call, Descriptor.WorkloadMethod);
EmitRuntimeAsyncAwait(ilBuilder, Descriptor.WorkloadMethod.ReturnType);
if (resultLocal is not null)
{
ilBuilder.EmitStloc(resultLocal);
ilBuilder.EmitLdloca(resultLocal);
ilBuilder.Emit(OpCodes.Call, KeepAliveWithoutBoxingInMethod.MakeGenericMethod(resultLocal.LocalType));
}

// if (--invokeCount >= 0) goto callBenchmarkLabel;
ilBuilder.MarkLabel(callBenchmarkLoopLabel);
ilBuilder.Emit(OpCodes.Ldarg_0);
ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField);
ilBuilder.Emit(OpCodes.Ldflda, invokeCountField);
ilBuilder.Emit(OpCodes.Dup);
ilBuilder.Emit(OpCodes.Ldind_I8);
ilBuilder.Emit(OpCodes.Ldc_I4_1);
ilBuilder.Emit(OpCodes.Conv_I8);
ilBuilder.Emit(OpCodes.Sub);
ilBuilder.EmitStloc(invokeCountLocal);
ilBuilder.EmitLdloc(invokeCountLocal);
ilBuilder.Emit(OpCodes.Stind_I8);
ilBuilder.EmitLdloc(invokeCountLocal);
ilBuilder.Emit(OpCodes.Ldc_I4_0);
ilBuilder.Emit(OpCodes.Conv_I8);
ilBuilder.Emit(OpCodes.Bge, callBenchmarkLabel);

// if (!await workloadValueTaskSource.SetResultAndGetIsComplete(startedClock.GetElapsed())) goto startClockLabel;
EmitLoadWorkloadValueTaskSource();
ilBuilder.EmitLdloca(startedClockLocal);
ilBuilder.Emit(OpCodes.Call, typeof(StartedClock).GetMethod(nameof(StartedClock.GetElapsed), BindingFlags.Public | BindingFlags.Instance)!);
ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadValueTaskSource).GetMethod(nameof(WorkloadValueTaskSource.SetResultAndGetIsComplete), BindingFlags.Public | BindingFlags.Instance)!);
EmitRuntimeAsyncAwait(ilBuilder, typeof(ValueTask<bool>));
ilBuilder.Emit(OpCodes.Brfalse, startClockLabel);
// return default;
ilBuilder.MaybeEmitSetLocalToDefault(returnLocal);
ilBuilder.Emit(OpCodes.Leave, returnLabel);
}
ilBuilder.BeginCatchBlock(typeof(Exception));
{
// workloadValueTaskSource.SetException(exception);
ilBuilder.EmitStloc(exceptionLocal);
EmitLoadWorkloadValueTaskSource();
ilBuilder.EmitLdloc(exceptionLocal);
ilBuilder.Emit(OpCodes.Callvirt, typeof(WorkloadValueTaskSource).GetMethod(nameof(WorkloadValueTaskSource.SetException), [typeof(Exception)])!);
// return default;
ilBuilder.MaybeEmitSetLocalToDefault(returnLocal);
ilBuilder.Emit(OpCodes.Leave, returnLabel);
}
ilBuilder.EndExceptionBlock();

ilBuilder.MarkLabel(returnLabel);
if (returnLocal is not null)
{
ilBuilder.EmitLdloc(returnLocal);
}
ilBuilder.Emit(OpCodes.Ret);

return methodBuilder;

void EmitLoadWorkloadValueTaskSource()
{
ilBuilder.Emit(OpCodes.Ldarg_0);
ilBuilder.Emit(OpCodes.Ldflda, fieldsContainerField);
ilBuilder.Emit(OpCodes.Ldfld, workloadValueTaskSourceField);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
#if NET9_0_OR_GREATER
using System.Runtime.Loader;
#endif
using System.Security;
using static BenchmarkDotNet.Code.RunnableConstants;
using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers;
Expand Down Expand Up @@ -91,21 +94,32 @@ public static Assembly EmitPartitionAssembly(ArtifactsPaths artifactsPaths, Buil
runnableEmitter.EmitRunnableCore();
}

if (saveToDisk)
{
assemblyBuilder.Save(assemblyFileName);
logger.WriteLineInfo($"{assemblyFileName} assembly saved to {assemblyResultPath}");
}

if (!saveToDisk)
return assemblyBuilder;

#if NET9_0_OR_GREATER
// A persisted assembly can't run, so the saved image is loaded back next to the benchmarks.
using var stream = new MemoryStream();
((PersistedAssemblyBuilder)assemblyBuilder).Save(stream);
File.WriteAllBytes(assemblyResultPath, stream.ToArray());
logger.WriteLineInfo($"{assemblyFileName} assembly saved to {assemblyResultPath}");

stream.Position = 0;
var benchmarksAssembly = buildPartition.RepresentativeBenchmarkCase.Descriptor.Type.Assembly;
return (AssemblyLoadContext.GetLoadContext(benchmarksAssembly) ?? AssemblyLoadContext.Default).LoadFromStream(stream);
#else
assemblyBuilder.Save(assemblyFileName);
logger.WriteLineInfo($"{assemblyFileName} assembly saved to {assemblyResultPath}");
return assemblyBuilder;
#endif
}

private static bool ShouldSaveToDisk(IConfig config)
{
if (!BenchmarkDotNetInfo.Instance.IsRelease)
{
// we never want to do that in our official NuGet.org package, it's a hack
return config.Options.IsSet(ConfigOptions.KeepBenchmarkFiles) && Portability.RuntimeInformation.IsFullFramework;
return config.Options.IsSet(ConfigOptions.KeepBenchmarkFiles);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it safe to remove IsFullFramework check here?

It looks like IsFullFramework check exists to avoid issue assemblyBuilder.Save is not supported on .NET Core.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path is only reached with debug BDN, the only way for it to fail is if someone goes out of their way to test it locally on an older tfm. So it's safe enough, not worth the extra code.

}

return false;
Expand All @@ -120,6 +134,15 @@ private static AssemblyBuilder DefineAssemblyBuilder(string assemblyResultPath,
{
var assemblyName = new AssemblyName { Name = Path.GetFileNameWithoutExtension(assemblyResultPath) };

#if NET9_0_OR_GREATER
if (saveToDisk)
{
var persistedAssemblyBuilder = new PersistedAssemblyBuilder(assemblyName, typeof(object).Assembly);
DefineAssemblyAttributes(persistedAssemblyBuilder);
return persistedAssemblyBuilder;
}
#endif

var assemblyMode = saveToDisk
? (AssemblyBuilderAccess)3 // https://apisof.net/catalog/System.Reflection.Emit.AssemblyBuilderAccess.RunAndSave
: AssemblyBuilderAccess.RunAndCollect;
Expand Down Expand Up @@ -174,9 +197,13 @@ private static ModuleBuilder DefineModuleBuilder(AssemblyBuilder assemblyBuilder
var moduleName = Path.GetFileNameWithoutExtension(moduleFileName)
?? throw new ArgumentNullException(nameof(moduleFileName));

#if NET9_0_OR_GREATER
var moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName);
#else
var moduleBuilder = saveToDisk
? assemblyBuilder.DefineDynamicModule(moduleName, moduleFileName)
: assemblyBuilder.DefineDynamicModule(moduleName);
#endif

// [module:UnverifiableCodeAttribute()]
var attributeCtor = typeof(UnverifiableCodeAttribute)
Expand Down
Loading
Loading