代码之家  ›  专栏  ›  技术社区  ›  Matthew Watson

是否可以使用#if NET6_0_或_更大来从BenchmarkDotNet运行中排除基准方法?

  •  3
  • Matthew Watson  · 技术社区  · 3 年前

    假设您正在编写一些与BenchmarkDotNet一起使用的多目标基准测试 net48 net6.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 目标和输出如下:

    |            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> 然后(在上面的例子中) 使用span() 方法是 排除 对于这两个目标,结果输出如下:

    |            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 |    
    

    我必须解决这个问题,只针对项目,编辑项目文件,分别针对框架,然后分别为每个目标运行基准测试。

    有没有一种方法可以让这项工作在多目标项目中发挥作用?


    为了完整起见,这里有一个完整的可编译测试应用程序,演示了这个问题。我正在使用VisualStudio2022。

    项目文件:

    <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];
    }
    
    0 回复  |  直到 3 年前
        1
  •  2
  •   Matthew Watson    3 年前

    从内存,基准测试。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(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 |
    

    (顺便说一句,一个有趣的结果是 UsingBitConverter() 方法实际上似乎运行得更快 net48 与…相比 net6.0 -虽然这与 Span<T> .)

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

        2
  •  1
  •   Adam Sitnik    3 年前

    这一点已经在本书中讨论过 https://github.com/dotnet/BenchmarkDotNet/issues/1226#issuecomment-532144829 :

    当运行以XYZ框架为目标的主机进程时,BDN使用反射来获取可用方法(基准)的列表。如果你正在使用 #if 定义,那么每个主机进程目标框架的基准列表将不同。

    性能回购文件描述了如何比较多个运行时的性能: https://github.com/dotnet/performance/blob/master/docs/benchmarkdotnet.md#multiple-runtimes

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

    您可以在中阅读有关多个TFM基准测试的更多信息 https://benchmarkdotnet.org/articles/configs/toolchains.html#multiple-frameworks-support

    推荐文章