alexeyfv

Опубликовано

- 3 мин чтения

Добавляем кастомные столбцы в результаты BenchmarkDotNet

C# .NET BenchmarkDotNet Performance Benchmarks
img of Добавляем кастомные столбцы в результаты BenchmarkDotNet

В BenchmarkDotNet (BDN) есть возможность добавлять столбцы в результаты выполнения бенчмарка (summary). Это сделать просто, когда значения известны заранее. Но если значение вычисляется прямо во время выполнения бенчмарка, вывести его в результатах — задача не тривиальная. В этой статье разберём, почему это так и разберём решение на базе IInProcessDiagnoser и генераторов кода.

Зачем вообще добавлять столбцы в результаты?

BDN автоматически добавляет столбцы в результаты при использовании атрибутов [Params] или [ParamsSource]. Это отлично работает, если вы знаете значения параметров заранее. Например, в статье о производительности FrozenDictionary я использовал [ParamsSource] для изменения размера словаря (DictionarySize) и замерял время выполнения при таком размере:

  | Method           | DictionarySize |            Mean |    Ratio |
| ---------------- | -------------- | --------------: | -------: |
| Dictionary       | 1000000        | 7,345,645.62 ns | baseline |
| FrozenDictionary | 1000000        | 4,712,357.84 ns |     -36% |

Задача усложняется, когда значение, которое вы хотите показать в результатах, генерируется во время выполнения бенчмарка. Например, вы написали бенчмарк для SQL-запроса и хотите отобразить план запроса или ключевые метрики из него рядом с метриками времени.

Почему это сложно сделать?

По умолчанию BDN запускает бенчмарки в отдельном процессе. Изоляция на уровне процесса делает измерения более точными, поскольку уменьшает влияние от хост-процесса.

Изоляция процессов в BenchmarkDotNet

Из-за этого измерения выполняются внутри процесса бенчмарка, но результаты формируются в хост-процессе. Простого способа передачи пользовательских данных из процесса бенчмарка в хост-процесс в BDN до недавного времени не было. Это известное ограничение, существующее с 2018 года. К счастью, in-process diagnosers были совсем недавно добавлены в BDN v0.16.

Как могут помочь in-process diagnosers

Используя IInProcessDiagnoser с IInProcessDiagnoserHandler, можно передавать данные из процесса бенчмарка в хост-процесс. После чего, можно отобразить их реализовав интерфейс IColumn.

Добавление собственного столбца в результаты состоит из следующих шагов:

  • реализовать IInProcessDiagnoser;
  • реализовать IInProcessDiagnoserHandler;
  • реализовать один или несколько экземпляров IColumn для каждого столбца, который вы хотите видеть в результатах;
  • создать конфигурацию, реализовав абстрактный класс ManualConfig и добавить туда ваши столбцы;
  • зарегистрировать конфигурацию, чтобы BenchmarkDotNet использовал её.

Поскольку добавление даже одного столбца требует написания большого количества бойлерплейта, я создал библиотеку на основе генераторов кода.

Я оставил комментарий об этой библиотеке в issue BDN, потому что, по моему мнению, этот функционал должен быть встроенным в BDN. Пока что эта библиотека может использоваться для уменьшения бойлерплейта. Если/когда в BDN добавят такой функционал, я планирую пометить библиотеку как deprecated.

Как использовать библиотеку

Библиотека основана на функциональности BDN, которая доступна только в nightly-сборках, поэтому сперва вам нужно добавить nightly-фид BDN в NuGet-источники.

Добавляем nightly-фид BenchmarkDotNet

Создайте файл nuget.config в корне вашего решения со следующим кодом:

  <?xml version="1.0" encoding="utf-8"?>
<configuration>
    <packageSources>
        <!-- Official NuGet feed -->
        <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
        <!-- BenchmarkDotNet nightly feed -->
        <add key="bdn-nightly" value="https://www.myget.org/F/benchmarkdotnet/api/v3/index.json" />
    </packageSources>
</configuration>

Устанавливаем пакеты

Добавьте nighly-версию BDN и библиотеку с генераторами исходного кода в ваш проект:

  dotnet add package BenchmarkDotNet --version 0.16.0-nightly.20260105.397
dotnet add package AlekseiFedorov.BenchmarkDotNet.ReportColumns

Добавляем атрибуты на свойства

Сделайте класс бенчмарка partial и добавьте атрибут [ReportColumn] на свойства, которые вы хотите видеть в результатах:

  public partial class MyBenchmark
{
    [ReportColumn]
    public int MyAwesomeColumn { get; set; }

    [Benchmark]
    public async Task BenchmarkMethod()
    {
        MyAwesomeColumn = Random.Shared.Next(1, 100);
        await Task.Delay(1000);
    }
}

По умолчанию генератор создаст столбец с именем свойства в качестве заголовка и будет использовать последнее наблюдаемое значение как значение столбца.

  | Method          |    Mean |    Error |   StdDev | MyAwesomeColumn |
| --------------- | ------: | -------: | -------: | --------------- |
| BenchmarkMethod | 1.000 s | 0.0001 s | 0.0001 s | 77              |

Смотрим на сгенерированные файлы

Если вы хотите посмотреть сгенерированные файлы, добавьте следующие строки в ваш файл бенчмарка .csproj:

  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>

После сборки перейдите в указанную папку. Вы должны увидеть файлы, наподобие этих:

  public sealed class MyBenchmark_MyAwesomeColumn_Column : IColumn
{
    // Код для столбца
}

public sealed class MyBenchmark_InProcessDiagnoser : IInProcessDiagnoser
{
    // Код in-process diagnoser
}

public sealed class MyBenchmark_InProcessDiagnoserHandler : IInProcessDiagnoserHandler
{
    // Код хендлера
}

public sealed class MyBenchmark_ManualConfig : ManualConfig
{
    public MyBenchmark_ManualConfig()
    {
        AddDiagnoser(new MyBenchmark_InProcessDiagnoser());
        AddColumn(new MyBenchmark_MyAwesomeColumn_Column());
    }
}

internal static class MyBenchmark_Results
{
    // Класс для хранения результатов бенчмарка
}

namespace Demo
{
    [Config(typeof(MyBenchmark_ManualConfig))]
    public partial class MyBenchmark
    {
    }
}

Заключение

IInProcessDiagnoser позволяет передавать данные из процесса бенчмарка в хост-процесс и отображать их в результатах с помощью IColumn. Однако добавление таких слобцов вручную требует написания большого количества бойлерплейта. Пакет NuGet AlekseiFedorov.BenchmarkDotNet.ReportColumns позволяет уменьшить бойлерплейт до минимума, чтобы вы могли сосредоточиться на бенчмаркинге.