是否可以使用 #if NET6_0_OR_GREATER 从 BenchmarkDotNet 运行 中排除基准方法?
Is it possible to use #if NET6_0_OR_GREATER to exclude a benchmark method from a BenchmarkDotNet run?
假设您正在编写一些与 BenchmarkDotNet 一起使用的基准测试,这些基准测试针对 net48
和 net6.0
,并且这些基准测试之一只能针对 [=18] =]目标。
显而易见的事情是使用类似这样的东西从 net48
构建中排除该特定基准:
#if NET6_0_OR_GREATER
[Benchmark]
public void UsingSpan()
{
using var stream = new MemoryStream();
writeUsingSpan(stream, _array);
}
static void writeUsingSpan(Stream output, double[] array)
{
var span = array.AsSpan();
var bytes = MemoryMarshal.AsBytes(span);
output.Write(bytes);
}
#endif // NET6_0_OR_GREATER
不幸的是,这不起作用,它不起作用的方式取决于项目文件中 TargetFrameworks
属性 中指定的目标的顺序。
如果您订购框架以便 net6.0
首先是 <TargetFrameworks>net6.0;net48</TargetFrameworks>
然后(在上面的示例中)UsingSpan()
方法包含在两个目标中,导致 BenchmarkDotNet 构建错误对于 net48
目标和输出,例如:
| Method | Job | Runtime | Mean | Error | StdDev |
|------------------ |------------------- |------------------- |-----------:|----------:|----------:|
| UsingBitConverter | .NET 6.0 | .NET 6.0 | 325.587 us | 2.0160 us | 1.8858 us |
| UsingMarshal | .NET 6.0 | .NET 6.0 | 505.784 us | 4.3719 us | 4.0894 us |
| UsingSpan | .NET 6.0 | .NET 6.0 | 4.942 us | 0.0543 us | 0.0482 us |
| UsingBitConverter | .NET Framework 4.8 | .NET Framework 4.8 | NA | NA | NA |
| UsingMarshal | .NET Framework 4.8 | .NET Framework 4.8 | NA | NA | NA |
| UsingSpan | .NET Framework 4.8 | .NET Framework 4.8 | NA | NA | NA |
另一方面,如果您订购框架,那么 net48
首先是 <TargetFrameworks>net48;net6.0</TargetFrameworks>
然后(在上面的示例中)UsingSpan()
方法是 excluded 两个目标,结果输出如下:
| Method | Job | Runtime | Mean | Error | StdDev |
|------------------ |------------------- |------------------- |---------:|---------:|---------:|
| UsingBitConverter | .NET 6.0 | .NET 6.0 | 343.1 us | 6.51 us | 11.57 us |
| UsingMarshal | .NET 6.0 | .NET 6.0 | 539.5 us | 10.77 us | 22.94 us |
| UsingBitConverter | .NET Framework 4.8 | .NET Framework 4.8 | 331.2 us | 5.43 us | 5.08 us |
| UsingMarshal | .NET Framework 4.8 | .NET Framework 4.8 | 588.9 us | 11.18 us | 10.98 us |
我必须通过单一目标项目并编辑项目文件以分别针对框架,然后 运行 每个目标单独的基准来解决这个问题。
有没有办法让它在多目标项目中工作?
为了完整起见,这里有一个完整的可编译测试应用程序来演示该问题。我正在使用 Visual Studio 2022.
项目文件:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net48;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
</ItemGroup>
“Program.cs”文件:
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
namespace Benchmark;
public static class Program
{
public static void Main()
{
BenchmarkRunner.Run<UnderTest>();
}
}
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTest
{
[Benchmark]
public void UsingBitConverter()
{
using var stream = new MemoryStream();
writeUsingBitConverter(stream, _array);
}
static void writeUsingBitConverter(Stream output, double[] array)
{
foreach (var sample in array)
{
output.Write(BitConverter.GetBytes(sample), 0, sizeof(double));
}
}
[Benchmark]
public void UsingMarshal()
{
using var stream = new MemoryStream();
writeUsingMarshal(stream, _array);
}
static void writeUsingMarshal(Stream output, double[] array)
{
const int SIZE_BYTES = sizeof(double);
byte[] buffer = new byte[SIZE_BYTES];
IntPtr ptr = Marshal.AllocHGlobal(SIZE_BYTES);
foreach (var sample in array)
{
Marshal.StructureToPtr(sample, ptr, true);
Marshal.Copy(ptr, buffer, 0, SIZE_BYTES);
output.Write(buffer, 0, SIZE_BYTES);
}
Marshal.FreeHGlobal(ptr);
}
#if NET6_0_OR_GREATER
[Benchmark]
public void UsingSpan()
{
using var stream = new MemoryStream();
writeUsingSpan(stream, _array);
}
static void writeUsingSpan(Stream output, double[] array)
{
var span = array.AsSpan();
var bytes = MemoryMarshal.AsBytes(span);
output.Write(bytes);
}
#endif // NET6_0_OR_GREATER
readonly double[] _array = new double[10_000];
}
根据记忆,Benchmark.NET 将 运行 对所有具有一些内部魔法的框架进行基准测试。因此,与其使用现有的 ,不如将测试拆分为具有不同 RuntimeMoniker
属性的两个 classes。例如:
[SimpleJob(RuntimeMoniker.Net48)]
public class UnderTestNet48
{
// Benchmarks
}
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTestNet60
{
// Benchmarks
}
现在您需要修改 运行 基准测试的代码,因为它们被拆分为 classes,像这样的东西会起作用:
public static void Main()
{
var config = DefaultConfig.Instance.
.WithOptions(ConfigOptions.JoinSummary)
.WithOptions(ConfigOptions.DisableLogFile);
BenchmarkRunner.Run(typeof(Program).Assembly, config);
}
[从 OP (Matthew Watson) 编辑]
多亏了这个答案,我才得以实现。
我设法通过将常用测试方法放入受保护的基础 class 中来减少代码重复,然后提供两个派生的 classes - 一个用于 net48
基准测试一个用于 net5.0
基准。
这是我最终得到的代码:
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
namespace Benchmark;
public static class Program
{
public static void Main()
{
BenchmarkRunner.Run(
typeof(Program).Assembly,
DefaultConfig.Instance
.WithOptions(ConfigOptions.JoinSummary)
.WithOptions(ConfigOptions.DisableLogFile));
}
}
public abstract class UnderTestBase
{
protected static Stream CreateStream()
{
return new MemoryStream(); // Or Stream.Null
}
protected void WriteUsingBitConverter(Stream output, double[] array)
{
foreach (var sample in array)
{
output.Write(BitConverter.GetBytes(sample), 0, sizeof(double));
}
}
protected void WriteUsingMarshal(Stream output, double[] array)
{
const int SIZE_BYTES = sizeof(double);
byte[] buffer = new byte[SIZE_BYTES];
IntPtr ptr = Marshal.AllocHGlobal(SIZE_BYTES);
foreach (var sample in array)
{
Marshal.StructureToPtr(sample, ptr, true);
Marshal.Copy(ptr, buffer, 0, SIZE_BYTES);
output.Write(buffer, 0, SIZE_BYTES);
}
Marshal.FreeHGlobal(ptr);
}
#if NET6_0_OR_GREATER
protected void WriteUsingSpan(Stream output, double[] array)
{
var span = array.AsSpan();
var bytes = MemoryMarshal.AsBytes(span);
output.Write(bytes);
}
#endif // NET6_0_OR_GREATER
protected readonly double[] Array = new double[100_000];
}
[SimpleJob(RuntimeMoniker.Net48)]
public class UnderTestNet48: UnderTestBase
{
[Benchmark]
public void UsingBitConverter()
{
using var stream = CreateStream();
WriteUsingBitConverter(stream, Array);
}
[Benchmark]
public void UsingMarshal()
{
using var stream = CreateStream();
WriteUsingMarshal(stream, Array);
}
}
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTestNet60: UnderTestBase
{
[Benchmark]
public void UsingBitConverter()
{
using var stream = CreateStream();
WriteUsingBitConverter(stream, Array);
}
[Benchmark]
public void UsingMarshal()
{
using var stream = CreateStream();
WriteUsingMarshal(stream, Array);
}
#if NET6_0_OR_GREATER
[Benchmark]
public void UsingSpan()
{
using var stream = CreateStream();
WriteUsingSpan(stream, Array);
}
#endif // NET6_0_OR_GREATER
}
导致此输出:
| Type | Method | Job | Runtime | Mean | Error | StdDev |
|--------------- |------------------ |------------------- |------------------- |-----------:|----------:|----------:|
| UnderTestNet60 | UsingBitConverter | .NET 6.0 | .NET 6.0 | 4,110.8 us | 81.53 us | 151.13 us |
| UnderTestNet60 | UsingMarshal | .NET 6.0 | .NET 6.0 | 5,774.0 us | 114.78 us | 194.90 us |
| UnderTestNet60 | UsingSpan | .NET 6.0 | .NET 6.0 | 521.6 us | 5.13 us | 4.80 us |
| UnderTestNet48 | UsingBitConverter | .NET Framework 4.8 | .NET Framework 4.8 | 2,987.2 us | 35.60 us | 29.73 us |
| UnderTestNet48 | UsingMarshal | .NET Framework 4.8 | .NET Framework 4.8 | 5,616.9 us | 57.85 us | 48.30 us |
(顺便说一句,一个有趣的结果是 UsingBitConverter()
方法实际上似乎 运行 与 net48
相比 net6.0
更快 - 尽管这与 net6.0
相形见绌Span<T>
带来的巨大速度提升。)
[/编辑自 OP (Matthew Watson)]
已在https://github.com/dotnet/BenchmarkDotNet/issues/1226#issuecomment-532144829中讨论过:
当运行主机进程以XYZ框架为目标时,BDN正在使用反射来获取可用方法(基准)的列表。如果您使用 #if
定义,则基准列表将因主机进程目标框架而异。
性能回购文档描述了如何在此处比较多个运行时性能:
https://github.com/dotnet/performance/blob/master/docs/benchmarkdotnet.md#multiple-runtimes
The host process needs to be the lowest common API denominator of the runtimes you want to compare!
您可以在 https://benchmarkdotnet.org/articles/configs/toolchains.html#multiple-frameworks-support
中阅读有关对多个 tfms 进行基准测试的更多信息
假设您正在编写一些与 BenchmarkDotNet 一起使用的基准测试,这些基准测试针对 net48
和 net6.0
,并且这些基准测试之一只能针对 [=18] =]目标。
显而易见的事情是使用类似这样的东西从 net48
构建中排除该特定基准:
#if NET6_0_OR_GREATER
[Benchmark]
public void UsingSpan()
{
using var stream = new MemoryStream();
writeUsingSpan(stream, _array);
}
static void writeUsingSpan(Stream output, double[] array)
{
var span = array.AsSpan();
var bytes = MemoryMarshal.AsBytes(span);
output.Write(bytes);
}
#endif // NET6_0_OR_GREATER
不幸的是,这不起作用,它不起作用的方式取决于项目文件中 TargetFrameworks
属性 中指定的目标的顺序。
如果您订购框架以便 net6.0
首先是 <TargetFrameworks>net6.0;net48</TargetFrameworks>
然后(在上面的示例中)UsingSpan()
方法包含在两个目标中,导致 BenchmarkDotNet 构建错误对于 net48
目标和输出,例如:
| Method | Job | Runtime | Mean | Error | StdDev |
|------------------ |------------------- |------------------- |-----------:|----------:|----------:|
| UsingBitConverter | .NET 6.0 | .NET 6.0 | 325.587 us | 2.0160 us | 1.8858 us |
| UsingMarshal | .NET 6.0 | .NET 6.0 | 505.784 us | 4.3719 us | 4.0894 us |
| UsingSpan | .NET 6.0 | .NET 6.0 | 4.942 us | 0.0543 us | 0.0482 us |
| UsingBitConverter | .NET Framework 4.8 | .NET Framework 4.8 | NA | NA | NA |
| UsingMarshal | .NET Framework 4.8 | .NET Framework 4.8 | NA | NA | NA |
| UsingSpan | .NET Framework 4.8 | .NET Framework 4.8 | NA | NA | NA |
另一方面,如果您订购框架,那么 net48
首先是 <TargetFrameworks>net48;net6.0</TargetFrameworks>
然后(在上面的示例中)UsingSpan()
方法是 excluded 两个目标,结果输出如下:
| Method | Job | Runtime | Mean | Error | StdDev |
|------------------ |------------------- |------------------- |---------:|---------:|---------:|
| UsingBitConverter | .NET 6.0 | .NET 6.0 | 343.1 us | 6.51 us | 11.57 us |
| UsingMarshal | .NET 6.0 | .NET 6.0 | 539.5 us | 10.77 us | 22.94 us |
| UsingBitConverter | .NET Framework 4.8 | .NET Framework 4.8 | 331.2 us | 5.43 us | 5.08 us |
| UsingMarshal | .NET Framework 4.8 | .NET Framework 4.8 | 588.9 us | 11.18 us | 10.98 us |
我必须通过单一目标项目并编辑项目文件以分别针对框架,然后 运行 每个目标单独的基准来解决这个问题。
有没有办法让它在多目标项目中工作?
为了完整起见,这里有一个完整的可编译测试应用程序来演示该问题。我正在使用 Visual Studio 2022.
项目文件:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net48;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
</ItemGroup>
“Program.cs”文件:
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
namespace Benchmark;
public static class Program
{
public static void Main()
{
BenchmarkRunner.Run<UnderTest>();
}
}
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTest
{
[Benchmark]
public void UsingBitConverter()
{
using var stream = new MemoryStream();
writeUsingBitConverter(stream, _array);
}
static void writeUsingBitConverter(Stream output, double[] array)
{
foreach (var sample in array)
{
output.Write(BitConverter.GetBytes(sample), 0, sizeof(double));
}
}
[Benchmark]
public void UsingMarshal()
{
using var stream = new MemoryStream();
writeUsingMarshal(stream, _array);
}
static void writeUsingMarshal(Stream output, double[] array)
{
const int SIZE_BYTES = sizeof(double);
byte[] buffer = new byte[SIZE_BYTES];
IntPtr ptr = Marshal.AllocHGlobal(SIZE_BYTES);
foreach (var sample in array)
{
Marshal.StructureToPtr(sample, ptr, true);
Marshal.Copy(ptr, buffer, 0, SIZE_BYTES);
output.Write(buffer, 0, SIZE_BYTES);
}
Marshal.FreeHGlobal(ptr);
}
#if NET6_0_OR_GREATER
[Benchmark]
public void UsingSpan()
{
using var stream = new MemoryStream();
writeUsingSpan(stream, _array);
}
static void writeUsingSpan(Stream output, double[] array)
{
var span = array.AsSpan();
var bytes = MemoryMarshal.AsBytes(span);
output.Write(bytes);
}
#endif // NET6_0_OR_GREATER
readonly double[] _array = new double[10_000];
}
根据记忆,Benchmark.NET 将 运行 对所有具有一些内部魔法的框架进行基准测试。因此,与其使用现有的 RuntimeMoniker
属性的两个 classes。例如:
[SimpleJob(RuntimeMoniker.Net48)]
public class UnderTestNet48
{
// Benchmarks
}
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTestNet60
{
// Benchmarks
}
现在您需要修改 运行 基准测试的代码,因为它们被拆分为 classes,像这样的东西会起作用:
public static void Main()
{
var config = DefaultConfig.Instance.
.WithOptions(ConfigOptions.JoinSummary)
.WithOptions(ConfigOptions.DisableLogFile);
BenchmarkRunner.Run(typeof(Program).Assembly, config);
}
[从 OP (Matthew Watson) 编辑]
多亏了这个答案,我才得以实现。
我设法通过将常用测试方法放入受保护的基础 class 中来减少代码重复,然后提供两个派生的 classes - 一个用于 net48
基准测试一个用于 net5.0
基准。
这是我最终得到的代码:
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
namespace Benchmark;
public static class Program
{
public static void Main()
{
BenchmarkRunner.Run(
typeof(Program).Assembly,
DefaultConfig.Instance
.WithOptions(ConfigOptions.JoinSummary)
.WithOptions(ConfigOptions.DisableLogFile));
}
}
public abstract class UnderTestBase
{
protected static Stream CreateStream()
{
return new MemoryStream(); // Or Stream.Null
}
protected void WriteUsingBitConverter(Stream output, double[] array)
{
foreach (var sample in array)
{
output.Write(BitConverter.GetBytes(sample), 0, sizeof(double));
}
}
protected void WriteUsingMarshal(Stream output, double[] array)
{
const int SIZE_BYTES = sizeof(double);
byte[] buffer = new byte[SIZE_BYTES];
IntPtr ptr = Marshal.AllocHGlobal(SIZE_BYTES);
foreach (var sample in array)
{
Marshal.StructureToPtr(sample, ptr, true);
Marshal.Copy(ptr, buffer, 0, SIZE_BYTES);
output.Write(buffer, 0, SIZE_BYTES);
}
Marshal.FreeHGlobal(ptr);
}
#if NET6_0_OR_GREATER
protected void WriteUsingSpan(Stream output, double[] array)
{
var span = array.AsSpan();
var bytes = MemoryMarshal.AsBytes(span);
output.Write(bytes);
}
#endif // NET6_0_OR_GREATER
protected readonly double[] Array = new double[100_000];
}
[SimpleJob(RuntimeMoniker.Net48)]
public class UnderTestNet48: UnderTestBase
{
[Benchmark]
public void UsingBitConverter()
{
using var stream = CreateStream();
WriteUsingBitConverter(stream, Array);
}
[Benchmark]
public void UsingMarshal()
{
using var stream = CreateStream();
WriteUsingMarshal(stream, Array);
}
}
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTestNet60: UnderTestBase
{
[Benchmark]
public void UsingBitConverter()
{
using var stream = CreateStream();
WriteUsingBitConverter(stream, Array);
}
[Benchmark]
public void UsingMarshal()
{
using var stream = CreateStream();
WriteUsingMarshal(stream, Array);
}
#if NET6_0_OR_GREATER
[Benchmark]
public void UsingSpan()
{
using var stream = CreateStream();
WriteUsingSpan(stream, Array);
}
#endif // NET6_0_OR_GREATER
}
导致此输出:
| Type | Method | Job | Runtime | Mean | Error | StdDev |
|--------------- |------------------ |------------------- |------------------- |-----------:|----------:|----------:|
| UnderTestNet60 | UsingBitConverter | .NET 6.0 | .NET 6.0 | 4,110.8 us | 81.53 us | 151.13 us |
| UnderTestNet60 | UsingMarshal | .NET 6.0 | .NET 6.0 | 5,774.0 us | 114.78 us | 194.90 us |
| UnderTestNet60 | UsingSpan | .NET 6.0 | .NET 6.0 | 521.6 us | 5.13 us | 4.80 us |
| UnderTestNet48 | UsingBitConverter | .NET Framework 4.8 | .NET Framework 4.8 | 2,987.2 us | 35.60 us | 29.73 us |
| UnderTestNet48 | UsingMarshal | .NET Framework 4.8 | .NET Framework 4.8 | 5,616.9 us | 57.85 us | 48.30 us |
(顺便说一句,一个有趣的结果是 UsingBitConverter()
方法实际上似乎 运行 与 net48
相比 net6.0
更快 - 尽管这与 net6.0
相形见绌Span<T>
带来的巨大速度提升。)
[/编辑自 OP (Matthew Watson)]
已在https://github.com/dotnet/BenchmarkDotNet/issues/1226#issuecomment-532144829中讨论过:
当运行主机进程以XYZ框架为目标时,BDN正在使用反射来获取可用方法(基准)的列表。如果您使用 #if
定义,则基准列表将因主机进程目标框架而异。
性能回购文档描述了如何在此处比较多个运行时性能: https://github.com/dotnet/performance/blob/master/docs/benchmarkdotnet.md#multiple-runtimes
The host process needs to be the lowest common API denominator of the runtimes you want to compare!
您可以在 https://benchmarkdotnet.org/articles/configs/toolchains.html#multiple-frameworks-support
中阅读有关对多个 tfms 进行基准测试的更多信息