是否可以使用#if NET6_0_OR_GREATER从BenchmarkDotNet运行中排除基准测试方法



假设您正在编写一些用于BenchmarkDotNet的基准测试,这些基准测试是针对net48net6.0的多目标测试,并且其中一个基准测试只能针对net6.0目标进行编译。

显而易见的做法是使用这样的东西将特定的基准排除在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()方法包含在BOTH目标中,从而导致net48目标的BenchmarkDotNet构建错误,并输出如下:

|            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()方法都被排除在外,结果输出如下:

|            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属性的两个类。例如:

[SimpleJob(RuntimeMoniker.Net48)]
public class UnderTestNet48
{
// Benchmarks
}
[SimpleJob(RuntimeMoniker.Net60)]
public class UnderTestNet60
{
// Benchmarks
}

现在你需要修改运行基准测试的代码,因为它们是跨类划分的,这样的东西会起作用:

public static void Main()
{
var config = DefaultConfig.Instance.
.WithOptions(ConfigOptions.JoinSummary)
.WithOptions(ConfigOptions.DisableLogFile);
BenchmarkRunner.Run(typeof(Program).Assembly, config);
}

[编辑自OP(Matthew Watson(]

多亏了这个答案,我才得以实现。

我设法通过将常见的测试方法放入一个受保护的基类中,然后提供两个派生类来减少代码重复——一个用于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 |

(顺便说一句,一个有趣的结果是,与net6.0相比,net48UsingBitConverter()方法实际上运行得更快——尽管这与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-运行时

主机进程需要是要比较的运行时的最低公共API分母!

您可以在https://benchmarkdotnet.org/articles/configs/toolchains.html#multiple-支持的框架

相关内容

  • 没有找到相关文章

最新更新