【问题标题】:Is it possible to use #if NET6_0_OR_GREATER to exclude a benchmark method from a BenchmarkDotNet run?是否可以使用 #if NET6_0_OR_GREATER 从 BenchmarkDotNet 运行中排除基准方法?
【发布时间】:2022-01-13 09:37:23
【问题描述】:

假设您正在编写一些与 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() 方法包含在两个目标中,导致 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];
}

【问题讨论】:

  • 这看起来像是 BenchmarkDotNet 中的一个错误。我建议在项目的 github 中创建一个问题。
  • 您可能还需要将#if NET6_0_OR_GREATER 放在[SimpleJob(RuntimeMoniker.Net60)] 属性周围。
  • @DavidG 我会尝试一下并报告。
  • @DavidG 不幸的是,这并没有帮助 - 结果是一样的。
  • 好的,下一个选项...创建两个不同的类,一个使用RuntimeMoniker.Net60,一个使用RuntimeMoniker.Net48,然后使用BenchmarkRunner.Run(typeof(Program).Assembly, DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary).WithOptions(ConfigOptions.DisableLogFile));

标签: c# multitargeting benchmarkdotnet


【解决方案1】:

根据记忆,Benchmark.NET 将使用一些内部魔法为所有框架运行基准测试。因此,与其使用现有的preprocessor symbols,不如将​​测试拆分为具有不同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(马修·沃森)的编辑]

感谢这个答案,我设法实现了这一点。

我通过将通用测试方法放入受保护的基类中,然后提供两个派生类 - 一个用于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 相比 - 尽管这与 Span&lt;T&gt; 提供的速度上的巨大改进相形见绌。)

[/OP 编辑​​(马修·沃森)]

【讨论】:

    猜你喜欢
    • 2018-09-12
    • 1970-01-01
    • 2016-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多