Skip to content

Commit e3e774b

Browse files
committed
Added assembly weaver.
Reverted loop methods back to `AggressiveOptimization`. Added `NoInlining` to `__Overhead` to match weaved benchmark method. Updated ExpectedBenchmarkResultsTests.
1 parent 755031d commit e3e774b

File tree

25 files changed

+354
-132
lines changed

25 files changed

+354
-132
lines changed

BenchmarkDotNet.sln

+7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.P
5959
EndProject
6060
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting.Tests", "tests\BenchmarkDotNet.Exporters.Plotting.Tests\BenchmarkDotNet.Exporters.Plotting.Tests.csproj", "{199AC83E-30BD-40CD-87CE-0C838AC0320D}"
6161
EndProject
62+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Weaver", "src\BenchmarkDotNet.Weaver\BenchmarkDotNet.Weaver.csproj", "{5731DE42-16FE-430E-BA90-0EBE714CB221}"
63+
EndProject
6264
Global
6365
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6466
Debug|Any CPU = Debug|Any CPU
@@ -161,6 +163,10 @@ Global
161163
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.Build.0 = Debug|Any CPU
162164
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.ActiveCfg = Release|Any CPU
163165
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.Build.0 = Release|Any CPU
166+
{5731DE42-16FE-430E-BA90-0EBE714CB221}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
167+
{5731DE42-16FE-430E-BA90-0EBE714CB221}.Debug|Any CPU.Build.0 = Debug|Any CPU
168+
{5731DE42-16FE-430E-BA90-0EBE714CB221}.Release|Any CPU.ActiveCfg = Release|Any CPU
169+
{5731DE42-16FE-430E-BA90-0EBE714CB221}.Release|Any CPU.Build.0 = Release|Any CPU
164170
EndGlobalSection
165171
GlobalSection(SolutionProperties) = preSolution
166172
HideSolutionNode = FALSE
@@ -190,6 +196,7 @@ Global
190196
{2E2283A3-6DA6-4482-8518-99D6D9F689AB} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
191197
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
192198
{199AC83E-30BD-40CD-87CE-0C838AC0320D} = {14195214-591A-45B7-851A-19D3BA2413F9}
199+
{5731DE42-16FE-430E-BA90-0EBE714CB221} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
193200
EndGlobalSection
194201
GlobalSection(ExtensibilityGlobals) = postSolution
195202
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}

NuGet.Config

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
<clear />
99

1010
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
11-
<!-- reuquired to run Mono AOT benchmarks -->
11+
<!-- required to run Mono AOT benchmarks -->
1212
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
1313
<add key="dotnet7" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json" />
1414
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
1515
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
1616
<add key="dotnet10" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json" />
17+
18+
<add key="benchmarkdotnet.weaver" value="src/BenchmarkDotNet.Weaver/packages" />
1719
</packageSources>
1820
</configuration>

build/BenchmarkDotNet.Build/Program.cs

+21
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,29 @@ public static int Main(string[] args)
1616
}
1717
}
1818

19+
[TaskName(Name)]
20+
[TaskDescription("Pack Weaver")]
21+
public class PackWeaverTask : FrostingTask<BuildContext>, IHelpProvider
22+
{
23+
private const string Name = "pack-weaver";
24+
25+
public override void Run(BuildContext context) => context.BuildRunner.PackWeaver();
26+
27+
public HelpInfo GetHelp()
28+
{
29+
return new HelpInfo
30+
{
31+
Examples = new[]
32+
{
33+
new Example(Name)
34+
}
35+
};
36+
}
37+
}
38+
1939
[TaskName(Name)]
2040
[TaskDescription("Restore NuGet packages")]
41+
[IsDependentOn(typeof(PackWeaverTask))]
2142
public class RestoreTask : FrostingTask<BuildContext>, IHelpProvider
2243
{
2344
private const string Name = "restore";

build/BenchmarkDotNet.Build/Runners/BuildRunner.cs

+30-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Cake.Common.Tools.DotNet.Workload.Install;
99
using Cake.Core;
1010
using Cake.Core.IO;
11+
using System.Linq;
1112

1213
namespace BenchmarkDotNet.Build.Runners;
1314

@@ -20,6 +21,34 @@ public BuildRunner(BuildContext context)
2021
this.context = context;
2122
}
2223

24+
public void PackWeaver()
25+
{
26+
var weaverPath = context.AllPackableSrcProjects.Single(p => p.GetFilename() == "BenchmarkDotNet.Weaver.csproj");
27+
28+
context.DotNetRestore(weaverPath.GetDirectory().FullPath,
29+
new DotNetRestoreSettings
30+
{
31+
MSBuildSettings = context.MsBuildSettingsRestore
32+
});
33+
34+
context.Information("BuildSystemProvider: " + context.BuildSystem().Provider);
35+
context.DotNetBuild(weaverPath.FullPath, new DotNetBuildSettings
36+
{
37+
NoRestore = true,
38+
DiagnosticOutput = true,
39+
MSBuildSettings = context.MsBuildSettingsBuild,
40+
Configuration = context.BuildConfiguration,
41+
Verbosity = context.BuildVerbosity
42+
});
43+
44+
context.DotNetPack(weaverPath.FullPath, new DotNetPackSettings
45+
{
46+
OutputDirectory = weaverPath.GetDirectory().Combine("packages"),
47+
MSBuildSettings = context.MsBuildSettingsPack,
48+
Configuration = context.BuildConfiguration
49+
});
50+
}
51+
2352
public void Restore()
2453
{
2554
context.DotNetRestore(context.SolutionFile.FullPath,
@@ -71,7 +100,7 @@ public void Pack()
71100
var settingsSrc = new DotNetPackSettings
72101
{
73102
OutputDirectory = context.ArtifactsDirectory,
74-
ArgumentCustomization = args => args.Append("--include-symbols").Append("-p:SymbolPackageFormat=snupkg"),
103+
ArgumentCustomization = args => args.Append("--include-symbols").Append("-p:SymbolPackageFormat=snupkg").Append("-p:IsFullPack=true"),
75104
MSBuildSettings = context.MsBuildSettingsPack,
76105
Configuration = context.BuildConfiguration
77106
};

build/common.props

+5
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@
9090
</Code>
9191
</Task>
9292
</UsingTask>
93+
94+
<PropertyGroup>
95+
<!-- Increment this when the BenchmarkDotNet.Weaver package needs to be re-packed. -->
96+
<WeaverVersionSuffix>-1</WeaverVersionSuffix>
97+
</PropertyGroup>
9398
</Project>

samples/BenchmarkDotNet.Samples.FSharp/BenchmarkDotNet.Samples.FSharp.fsproj

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<!-- MSBuild was complaing about InformationalVersion from common.props file, so I excluded them in conditional way -->
44
<IsFsharp>true</IsFsharp>
55
</PropertyGroup>
6+
<Import Project="..\..\build\common.props" />
67
<PropertyGroup>
78
<OutputType>Exe</OutputType>
89
<TargetFrameworks>net462;net8.0</TargetFrameworks>

src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj

+15
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,19 @@
1414
<ItemGroup>
1515
<Compile Include="..\BenchmarkDotNet\Helpers\CodeAnnotations.cs" Link="Attributes\CodeAnnotations.cs" />
1616
</ItemGroup>
17+
18+
<Choose>
19+
<When Condition="'$(IsFullPack)' == 'true'">
20+
<!-- ProjectReference to ensure the nuget package has a dependency on BenchmarkDotNet.Weaver using the proper version. -->
21+
<ItemGroup>
22+
<ProjectReference Include="..\BenchmarkDotNet.Weaver\BenchmarkDotNet.Weaver.csproj" />
23+
</ItemGroup>
24+
</When>
25+
<Otherwise>
26+
<!-- PackageReference for transitive weaver dependency for ProjectReferences to this. -->
27+
<ItemGroup>
28+
<PackageReference Include="BenchmarkDotNet.Weaver" Version="$(Version)$(WeaverVersionSuffix)" />
29+
</ItemGroup>
30+
</Otherwise>
31+
</Choose>
1732
</Project>

src/BenchmarkDotNet.Weaver/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!packages/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!-- When any changes are made to this project, increment the WeaverVersionSuffix in the common.props file,
2+
then re-pack with output to \packages, and delete the old nupkg. -->
3+
4+
<Project Sdk="Microsoft.NET.Sdk">
5+
<Import Project="..\..\build\common.props" />
6+
<PropertyGroup>
7+
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
8+
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
9+
<OutputPath>$(MSBuildThisFileDirectory)bin\$(Configuration)</OutputPath>
10+
<VersionSuffix Condition="'$(IsFullPack)' != 'true'">$(VersionSuffix)$(WeaverVersionSuffix)</VersionSuffix>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Microsoft.Build.Framework" Version="17.12.6" />
15+
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
16+
<PackageReference Include="Mono.Cecil" Version="0.11.6" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<!-- Include .targets file and all DLLs in the output directory in the NuGet package -->
21+
<Content Include="$(MSBuildThisFileDirectory)buildTransitive\**\*.targets" Pack="true" PackagePath="buildTransitive" />
22+
<Content Include="$(OutputPath)**\*.dll" Exclude="$(OutputPath)**\$(AssemblyName).dll" Pack="true" PackagePath="lib/$(TargetFramework)" />
23+
<None Remove="packages\**" />
24+
</ItemGroup>
25+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<UsingTask TaskName="BenchmarkDotNet.Weaver.WeaveAssembliesTask" AssemblyFile="$(MSBuildThisFileDirectory)..\..\lib\net461\BenchmarkDotNet.Weaver.dll" />
4+
5+
<!-- .Net Framework causes "warning MSB3026: Could not copy ... The file is locked by: .NET Host" when the weaver task is ran on the user's project
6+
when the wrapper project is built. So DotNetCliCommand disables it globally, and the wrapper project enables it only for itself. -->
7+
<Target Name="WeaveAssemblies" AfterTargets="AfterBuild" Condition="'$(BenchmarkDotNetSkipWeaver)' != 'true' Or '$(BenchmarkDotNetWrapperProj)' == 'true'">
8+
<WeaveAssembliesTask TargetDir="$(TargetDir)" TargetAssembly="$(TargetDir)$(TargetFileName)" />
9+
</Target>
10+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<UsingTask TaskName="BenchmarkDotNet.Weaver.WeaveAssembliesTask" AssemblyFile="$(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\BenchmarkDotNet.Weaver.dll" />
4+
5+
<!-- .Net Framework causes "warning MSB3026: Could not copy ... The file is locked by: .NET Host" when the weaver task is ran on the user's project
6+
when the wrapper project is built. So DotNetCliCommand disables it globally, and the wrapper project enables it only for itself. -->
7+
<Target Name="WeaveAssemblies" AfterTargets="AfterBuild" Condition="'$(BenchmarkDotNetSkipWeaver)' != 'true' Or '$(BenchmarkDotNetWrapperProj)' == 'true'">
8+
<WeaveAssembliesTask TargetDir="$(TargetDir)" TargetAssembly="$(TargetDir)$(TargetFileName)" />
9+
</Target>
10+
</Project>
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using Microsoft.Build.Framework;
6+
using Microsoft.Build.Utilities;
7+
using Mono.Cecil;
8+
9+
namespace BenchmarkDotNet.Weaver;
10+
11+
internal class CustomAssemblyResolver : DefaultAssemblyResolver
12+
{
13+
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
14+
// Fix StackOverflow https://github.com/jbevain/cecil/issues/573
15+
=> name.FullName.StartsWith("netstandard") || name.FullName.StartsWith("mscorlib") || name.FullName.StartsWith("System.Private.CoreLib")
16+
? AssemblyDefinition.ReadAssembly(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), Path.ChangeExtension(name.Name, ".dll")), parameters)
17+
: base.Resolve(name, parameters);
18+
}
19+
20+
/// <summary>
21+
/// The Task used by MSBuild to weave the assemblies.
22+
/// </summary>
23+
public sealed class WeaveAssembliesTask : Task
24+
{
25+
/// <summary>
26+
/// The directory of the output.
27+
/// </summary>
28+
[Required]
29+
public string TargetDir { get; set; }
30+
31+
/// <summary>
32+
/// The path of the target assemblies.
33+
/// </summary>
34+
[Required]
35+
public string TargetAssembly { get; set; }
36+
37+
private readonly List<string> _warningMessages = [$"Benchmark methods were found in 1 or more assemblies that require NoInlining, and assembly weaving failed."];
38+
39+
/// <summary>
40+
/// Runs the weave assemblies task.
41+
/// </summary>
42+
/// <returns><see langword="true"/> if successful; <see langword="false"/> otherwise.</returns>
43+
public override bool Execute()
44+
{
45+
if (!File.Exists(TargetAssembly))
46+
{
47+
Log.LogError($"Assembly not found: {TargetAssembly}");
48+
return false;
49+
}
50+
51+
var resolver = new CustomAssemblyResolver();
52+
resolver.AddSearchDirectory(TargetDir);
53+
54+
// ReaderParameters { ReadWrite = true } is necessary to later write the file.
55+
// https://stackoverflow.com/questions/41840455/locked-target-assembly-with-mono-cecil-and-pcl-code-injection
56+
var readerParameters = new ReaderParameters
57+
{
58+
ReadWrite = true,
59+
AssemblyResolver = resolver
60+
};
61+
62+
ProcessAssembly(TargetAssembly, readerParameters, out bool isExecutable);
63+
64+
foreach (var assemblyPath in Directory.GetFiles(TargetDir, "*.dll"))
65+
{
66+
if (assemblyPath == TargetAssembly)
67+
{
68+
continue;
69+
}
70+
ProcessAssembly(assemblyPath, readerParameters, out _);
71+
}
72+
73+
// Assembly resolution can fail for library projects that contain references if the project does not have <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>.
74+
// Because the library project could be built as a dependency of the executable, we only log the warning if the target assembly is executable.
75+
if (_warningMessages.Count > 1 && isExecutable)
76+
{
77+
Log.LogWarning(string.Join(Environment.NewLine, _warningMessages));
78+
}
79+
return true;
80+
}
81+
82+
private void ProcessAssembly(string assemblyPath, ReaderParameters readerParameters, out bool isExecutable)
83+
{
84+
isExecutable = false;
85+
bool benchmarkMethodsImplAdjusted = false;
86+
try
87+
{
88+
using var module = ModuleDefinition.ReadModule(assemblyPath, readerParameters);
89+
isExecutable = module.EntryPoint != null;
90+
91+
foreach (var type in module.Types)
92+
{
93+
ProcessType(type, ref benchmarkMethodsImplAdjusted);
94+
}
95+
96+
// Write the modified assembly to file.
97+
module.Write();
98+
}
99+
catch (Exception e)
100+
{
101+
if (benchmarkMethodsImplAdjusted)
102+
{
103+
_warningMessages.Add($"Assembly: {assemblyPath}, error: {e.Message}");
104+
}
105+
}
106+
}
107+
108+
private void ProcessType(TypeDefinition type, ref bool benchmarkMethodsImplAdjusted)
109+
{
110+
// We can skip non-public types as they are not valid for benchmarks.
111+
if (type.IsNotPublic)
112+
{
113+
return;
114+
}
115+
116+
// Remove AggressiveInlining and add NoInlining to all [Benchmark] methods.
117+
foreach (var method in type.Methods)
118+
{
119+
if (method.CustomAttributes.Any(attr => attr.AttributeType.FullName == "BenchmarkDotNet.Attributes.BenchmarkAttribute"))
120+
{
121+
var oldImpl = method.ImplAttributes;
122+
method.ImplAttributes = (oldImpl & ~MethodImplAttributes.AggressiveInlining) | MethodImplAttributes.NoInlining;
123+
benchmarkMethodsImplAdjusted |= (oldImpl & MethodImplAttributes.NoInlining) == 0;
124+
}
125+
}
126+
127+
// Recursively process nested types
128+
foreach (var nestedType in type.NestedTypes)
129+
{
130+
ProcessType(nestedType, ref benchmarkMethodsImplAdjusted);
131+
}
132+
}
133+
}

src/BenchmarkDotNet/Helpers/Reflection.Emit/MethodBuilderExtensions.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using BenchmarkDotNet.Portability;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Reflection;
45
using System.Reflection.Emit;
@@ -33,5 +34,13 @@ public static MethodBuilder SetNoOptimizationImplementationFlag(this MethodBuild
3334

3435
return methodBuilder;
3536
}
37+
38+
public static MethodBuilder SetAggressiveOptimizationImplementationFlag(this MethodBuilder methodBuilder)
39+
{
40+
methodBuilder.SetImplementationFlags(
41+
methodBuilder.GetMethodImplementationFlags() | CodeGenHelper.AggressiveOptimizationOptionForEmit);
42+
43+
return methodBuilder;
44+
}
3645
}
3746
}
+3-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using System.Runtime.CompilerServices;
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
23

34
namespace BenchmarkDotNet.Portability
45
{
56
public static class CodeGenHelper
67
{
78
// AggressiveOptimization is not available in netstandard2.0, so just use the value casted to enum.
89
public const MethodImplOptions AggressiveOptimizationOption = (MethodImplOptions) 512;
10+
public const MethodImplAttributes AggressiveOptimizationOptionForEmit = (MethodImplAttributes) 512;
911
}
1012
}

0 commit comments

Comments
 (0)