Wojciech Nagórski

What I have done in .NET world.

Using native DLL and resource files in BenchmarkDotNet projects

In a previous post, I described the NativeMemoryProfiler which I implemented in the BenchmarkDotNet library. It caused that I got many questions about using native DLL files in benchmark projects. Many people want to use post-build events but they do not work. There is an issue dotnet/BenchmarkDotNet#946. I know a better solution, so I decided to describe this topic.

In this post, I would like to show you, how to use native DLL or resource files in benchmark projects.

If you only need solution, you can navigate to the solution section, but if you would like to understand the problem you need to read the whole article.

The story

When you run your benchmark project, the BenchmarkDotNet generates an isolated project that references to your project. This generated project is built and run, during each run of your benchmark. Let’s create a really simple benchmark project that shows us this mechanism.

In the beginning, we have to create a new project. I’m going to use dotnet CLI, so that you can follow the steps below on Windows, Linux or macOs.

mkdir UsingResourcesWithBenchmarkDotNet
cd UsingResourcesWithBenchmarkDotNet
dotnet new console

Then we need to add the BenchmarkDotNet nuget to this project.

dotnet add package BenchmarkDotNet

After configuring the project, we can implement our benchmark. Below is a Program.cs file:

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace UsingResourcesWithBenchmarkDotNet
{
    class Program
    {
        static void Main(string[] args) =>
            BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
    }

    public class Benchmark
    {
        [Benchmark]
        public int Sum()
        {
            int result = 0;
            for (int i = 0; i < 100; i++)
            {
                result += i;
            }

            return result;
        }
    }
}

Now we can run this project with a --keepfiles option. If you add this option, all auto-generated files will be kept after running the benchmark.

dotnet run -c Release -- --filter *Sum* --keepfiles

Now, bin directory should contain all generated files. In our case, we were run only one benchmark so all files should be in DefaultJob directory.

λ cd bin\Release\netcoreapp3.0\DefaultJob
λ ls  # or dir for windows console
BenchmarkDotNet.Autogenerated.csproj  DefaultJob.bat  DefaultJob.notcs  bin/  obj/

As you can see, BenchmarkDotNet generates a new project for us, calledBenchmarkDotNet.Autogenerated.csproj. For instance, when you want to run your benchmark for many frameworks, BenchmarkDotNet will generate many projects for you. Each generated project has its own bin directory and is run from this location.

Since BenchmarkDotNet does not run benchmarks from default bin directory, you can not use any recourses from there. Therefore, post-build events do not work because they are running during building the benchmark project, not auto-generated projects.

Solution

The solution is really simple. Instead of post-build events, you should use None or Compile project item. Below you can see an example of copying NativeDll.dll from solutionDir\x64\Release directory into bin directory of the benchmark project, as well as into bin directories of all auto-generated projects that refer to the benchmark project. If you set Visible to true, then the file will be visible in the solution explorer.

<ItemGroup>
  <None Include="$(MSBuildThisFileDirectory)..\x64\$(Configuration)\NativeDll.dll">
    <Link>NativeDll.dll</Link>
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <Visible>True</Visible>
  </None>
</ItemGroup>

What’s more, you do not have to add file by file. You can also copy the entire directory using this method.

  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)..\Resources\**\*.*">
      <Link>%(Directory)\%(Filename)%(Extension)</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <Visible>True</Visible>
    </None>
  </ItemGroup>

You can also copy all files of a given type, for example, all DLL files from a directory:

  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)..\Resources\**\*.dll">
      <Link>%(Directory)\%(Filename)%(Extension)</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <Visible>True</Visible>
    </None>
  </ItemGroup>

Summary

If you are creating a project with benchmarks using BenchmarkDotNet, you should include additional files as if you were creating a nuget package.

If you think this post is useful, let me know in the comments below.