Что такое BenchmarkDotNet и зачем она нужна?
BenchmarkDotNet — это мощная, кроссплатформенная библиотека с открытым исходным кодом для .NET, предназначенная для точного измерения производительности кода (бенчмаркинга). В мире разработки программного обеспечения часто возникает необходимость сравнить два или более подхода к решению одной задачи: какой алгоритм сортировки быстрее, какой способ форматирования строк эффективнее, как влияет использование разных структур данных на скорость работы приложения. BenchmarkDotNet решает эту задачу на профессиональном уровне.
Зачем она нужна? Ручное измерение времени с помощью Stopwatch чревато множеством ошибок: влияние фоновых процессов JIT-компиляции, сборки мусора (GC), прогрева кэша процессора, погрешности системного таймера. BenchmarkDotNet автоматически выполняет:
- Прогрев (Warmup): Запускает код несколько раз перед измерением, чтобы JIT-компилятор и кэш процессора вошли в стабильное состояние.
- Многократные запуски (Iterations): Выполняет метод множество раз и собирает статистику.
- Статистический анализ: Вычисляет среднее, медиану, стандартное отклонение, минимальное и максимальное время.
- Изоляцию: Запускает каждый бенчмарк в отдельном процессе, чтобы минимизировать влияние других тестов.
- Диагностику: Может измерять не только время, но и выделение памяти (Allocated), количество сборок мусора (GC Collections) и другие метрики.
Без BenchmarkDotNet вы рискуете получить недостоверные результаты и сделать неверные выводы об оптимизации кода.
Установка BenchmarkDotNet
Установка библиотеки BenchmarkDotNet в проект .NET (C#, F#, VB.NET) выполняется через менеджер пакетов NuGet. Существует три основных способа:
1. Через .NET CLI (рекомендуется)
Откройте терминал в корневой папке вашего проекта и выполните команду:
dotnet add package BenchmarkDotNet
2. Через Package Manager Console в Visual Studio
Откройте меню Tools -> NuGet Package Manager -> Package Manager Console и выполните:
Install-Package BenchmarkDotNet
3. Через графический интерфейс NuGet в Visual Studio
Кликните правой кнопкой мыши по проекту в Solution Explorer, выберите "Manage NuGet Packages", найдите "BenchmarkDotNet" и нажмите "Install".
Важно: Для корректной работы библиотека требует, чтобы проект был собран в Release конфигурации. Debug-конфигурация не оптимизирует код, что приведет к искаженным результатам.
Быстрый старт с примером кода
Давайте напишем простой бенчмарк, который сравнивает два способа конкатенации строк: через оператор + и через StringBuilder.
1. Создайте новый консольный проект .NET: dotnet new console -n MyBenchmark.
2. Установите библиотеку: dotnet add package BenchmarkDotNet.
3. Замените содержимое файла Program.cs на следующий код:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;
public class StringConcatenationBenchmark
{
private const int N = 1000; // Количество итераций внутри метода
[Benchmark]
public string UsingPlusOperator()
{
string result = "";
for (int i = 0; i < N; i++)
{
result = result + "a";
}
return result;
}
[Benchmark]
public string UsingStringBuilder()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < N; i++)
{
sb.Append("a");
}
return sb.ToString();
}
}
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<StringConcatenationBenchmark>();
}
}
4. Соберите проект в Release: dotnet run -c Release.
5. Запустите. BenchmarkDotNet выполнит серию тестов и выведет в консоль подробную таблицу с результатами, показывающую, что StringBuilder значительно быстрее и выделяет меньше памяти.
Основные методы и атрибуты BenchmarkDotNet
BenchmarkDotNet работает на основе атрибутов (Attributes) и методов. Вот список ключевых из них, которые необходимо знать каждому разработчику.
1. [Benchmark]
Что делает: Помечает метод, который будет измеряться. Это основной атрибут. Без него метод не будет участвовать в бенчмарке.
Параметры:
Baseline = true— делает этот метод эталонным для сравнения. Другие методы будут сравниваться с ним в процентах.Description = "string"— задает понятное имя для метода в результатах.
Рабочий пример:
[Benchmark(Baseline = true)]
public void MethodA() { /* ... */ }
[Benchmark(Description = "Быстрый метод")]
public void MethodB() { /* ... */ }
2. [Params]
Что делает: Позволяет запускать один и тот же бенчмарк с разными значениями параметров. Это автоматически создает строки в итоговой таблице для каждого значения.
Параметры: Принимает массив значений, которые будут переданы в свойство или поле класса.
Рабочий пример:
[Params(10, 100, 1000)]
public int IterationCount;
[Benchmark]
public void LoopTest()
{
for (int i = 0; i < IterationCount; i++) { /* ... */ }
}
3. [GlobalSetup]
Что делает: Метод, помеченный этим атрибутом, выполняется один раз перед запуском всех бенчмарков в классе. Используется для инициализации данных, которые не должны влиять на измеряемое время (например, создание большого массива).
Параметры: Нет.
Рабочий пример:
private int[] _data;
[GlobalSetup]
public void Setup()
{
_data = Enumerable.Range(0, 10000).ToArray();
}
[Benchmark]
public void SortData()
{
Array.Sort(_data);
}
4. [GlobalCleanup]
Что делает: Метод, помеченный этим атрибутом, выполняется один раз после завершения всех бенчмарков в классе. Используется для освобождения ресурсов (например, закрытие файлов или соединений с БД).
Параметры: Нет.
Рабочий пример:
private StreamWriter _writer;
[GlobalSetup]
public void Setup() => _writer = new StreamWriter("log.txt");
[GlobalCleanup]
public void Cleanup() => _writer.Dispose();
5. [IterationSetup] и [IterationCleanup]
Что делает: Выполняются перед и после каждой отдельной итерации бенчмарка. Используются реже, например, для сброса состояния между запусками, чтобы избежать накопления эффектов.
Параметры: Нет.
Рабочий пример:
private int _counter;
[IterationSetup]
public void ResetCounter() => _counter = 0;
[Benchmark]
public void Increment() => _counter++;
6. [MemoryDiagnoser]
Что делает: Атрибут класса. Включает диагностику памяти. После запуска бенчмарка в результатах появится столбец Allocated, показывающий, сколько байт памяти было выделено в управляемой куче.
Параметры: Нет.
Рабочий пример:
[MemoryDiagnoser]
public class MemoryBenchmark
{
[Benchmark]
public string CreateString() => new string('a', 100);
}
7. [SimpleJob]
Что делает: Атрибут класса или метода. Позволяет настроить параметры запуска бенчмарка: количество итераций, время прогрева, целевую платформу (Runtime). Это один из способов создания "джобов" (Jobs).
Параметры:
RuntimeMoniker— целевая среда выполнения (например,RuntimeMoniker.Net80).launchCount— количество запусков процесса.warmupCount— количество итераций прогрева.iterationCount— количество измеряемых итераций.
Рабочий пример:
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 3, iterationCount: 10)]
public class ConfigurableBenchmark { /* ... */ }
8. [BenchmarkCategory]
Что делает: Позволяет группировать бенчмарки по категориям. Это удобно, когда у вас много тестов и нужно запустить только определенную группу.
Параметры: Принимает одно или несколько строковых значений (категорий).
Рабочий пример:
[BenchmarkCategory("Fast")]
[Benchmark]
public void QuickMethod() { }
[BenchmarkCategory("Slow")]
[Benchmark]
public void HeavyMethod() { }
// Запуск только "Fast" категории:
// var config = ManualConfig.Create(DefaultConfig.Instance)
// .AddFilter(new CategoryFilter("Fast"));
9. [Arguments]
Что делает: Позволяет передавать аргументы напрямую в метод бенчмарка. В отличие от [Params], который применяется к полям/свойствам, [Arguments] работает с параметрами метода.
Параметры: Принимает объекты, которые будут переданы методу.
Рабочий пример:
[Benchmark]
[Arguments(100, "Hello")]
[Arguments(200, "World")]
public void MethodWithArgs(int count, string text)
{
for (int i = 0; i < count; i++) Console.Write(text);
}
10. [DryJob]
Что делает: Атрибут, который запускает бенчмарк в "сухом" режиме — всего по одной итерации прогрева и измерения. Используется для быстрой проверки, что бенчмарк вообще работает, без затрат времени на полный прогон.
Параметры: Нет.
Рабочий пример:
[DryJob]
public class QuickTest
{
[Benchmark]
public void Test() => Thread.Sleep(10);
}
Реальный практический пример использования
Предположим, мы разрабатываем систему обработки логов. Нам нужно выбрать между List<T> и LinkedList<T> для хранения записей, которые часто добавляются в конец и иногда удаляются из начала. Напишем бенчмарк, который симулирует эту нагрузку.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;
[MemoryDiagnoser]
public class CollectionBenchmark
{
private const int InitialSize = 1000;
private const int Operations = 10000;
private List<int> _list;
private LinkedList<int> _linkedList;
[GlobalSetup]
public void Setup()
{
_list = new List<int>(InitialSize);
_linkedList = new LinkedList<int>();
for (int i = 0; i < InitialSize; i++)
{
_list.Add(i);
_linkedList.AddLast(i);
}
}
[Benchmark(Baseline = true)]
public void ListAddRemove()
{
for