Suppose that you're writing some benchmarks for use with BenchmarkDotNet that are multi-targeted to net48
and net6.0
, and that one of those benchmarks can only be compiled for the net6.0
target.
The obvious thing to do is to use something like this to exclude that particular benchmark from the net48
build:
#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
This unfortunately doesn't work, and the way in which it doesn't work depends on the order of the targets specified in the TargetFrameworks
property in the project file.
If you order the frameworks so that net6.0
is first as <TargetFrameworks>net6.0;net48</TargetFrameworks>
then (in the example above) the UsingSpan()
method is included in BOTH targets, resulting in BenchmarkDotNet build errors for the net48
target and output such as this:
| 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 |
On the other hand, if you If order the frameworks so that net48
is first as <TargetFrameworks>net48;net6.0</TargetFrameworks>
then (in the example above) the UsingSpan()
method is excluded for both targets, resulting output such as this:
| 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 |
I have to solve this issue by single-targeting the project and editing the project file to target the frameworks separately, and then run the benchmarks separately for each target.
Is there a way to make this work with a multi-targeted project?
For completeness, here's a full compilable test app which demonstrates the issue. I'm using Visual Studio 2022.
The project file:
<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>
The "Program.cs" file:
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];
}