Найти тему

Бенчмарки. Измерение производительности и эффективности кода

Оглавление

Всем привет! Сегодня расскажу о новой для себя теме — бенчмарках, и как с их помощью измерять время выполнения и расход памяти.

Введение

Что такое бенчмарк? Это специальная библиотека, а так же созданное с её помощью приложение. В этом приложении мы пишем некоторое количество бенчмарк-тестов, для которых хотим сравнить время выполнения и расход памяти. После чего мы запускаем его, приложение (с помощью добавленной в него библиотеки) запускает наши тесты много раз, измеряет среднее время выполнения, расход памяти, количество мусора в разных поколениях сборщика и другие показатели (в зависимости от настройки) и выводит результаты в табличной форме на консоль.

Где применять

Бенчмарк довольно сильно нагружает процессор и память (даже в документации указано — при работе с ноутбука обязательно включать его в сеть, чтобы режим энергосбережения не замедлял работу и не давал ложные результаты). Тесты выполняются много раз, чтобы получить большой набор данных по результатам и исключить возможные выбросы в результатах.

Отсюда вывод — нельзя просто взять и прикрутить бенчмарк к нашему рабочему приложению (любому), потому что он предназначен для тестирования производительности. Оно проводится на этапе разработки для отдельных методов, а в самом приложении не проводится, потому что было бы странно в течение работы приложения запускать какой-либо метод 100 раз, чтобы посчитать скорость выполнения. Пользователь скажет "классно, что у вас так быстро считается всё, всего 30 наносекунд. Но почему я каждый раз жду этого подсчёта по полминуты".

Поэтому бенчмарки применяются для того, чтобы сравнить, какой из нескольких возможных подходов к выполнению какой-либо операции будет самым эффективным. Например, какой алгоритм быстрее отсортирует большой набор данных, какая способ выполнения операции отработает быстрее, каким образом мы обработаем больше запросов к серверу и так далее. Выбрав оптимальный способ, мы используем его в своём приложении

На чём будем проверять

В комментариях к прошлому посту об этой статье в моём телеграм-канале (не забывайте подписаться) возникла дискуссия о том, каким способом лучше сравнивать строки при сортировке листов по их номерам. Номера следует сортировать не по правилам сортировки строк, а по правилам сортировки чисел, но проблема в том, что в номерах строк могут быть символы, не являющиеся числами, и нам надо их отбросить.

Мне предложили использовать вместо моего способа (самописный IComparer для строк) использовать другие — встроенный Comparer из Windows, и Comparer из nuget-пакета NaturalStringComparer. Что ж, давайте попробуем создать бенчмарк-приложение и сравнить эти 3 способа:

Часть 1. Бенчмарк в консольном приложении

Как правило, для создания бенчмарков используется библиотека BechmarksDotNet. Создадим консольное приложение, добавим туда эту библиотеку и создадим пару бенчмарков. Жмём правой кнопкой мыши на проект — Управление пакетами nuget и устанавливаем нужную библиотеку:

Создадим класс StringBenchmark и для начала добавим к нему атрибуты:

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net48)]

Первый атрибут позволит нам увидеть в результатах ещё и расход памяти, помимо времени выполнения.

А с помощью второго и третьего атрибута мы сравним показатели выполнения для .NET 4.8 (применяется в Revit 2020-2024), и для .NET 8 (применяется в Revit 2025.

Далее определим набор строк, который мы будем сортировать. Я создал набор строк в обратном порядке, чтобы усложнить работу алгоритму сортировки:

private readonly string[] _numbers =
{
"99", "98", "97", "094", "93", "85", "81", "079", "73", "72", "60", "0052", "51", "50", "47",
"43", "42", "041", "35", "00030", "21", "019", "18", "11", "10", "09", "5", "4", "03", "1",
};

И определим 3 метода с атрибутом [Benchmark] для тестирования:

-2

Методы возвращают не void, а список строк, потому что бенчмарк работает в конфигурации Release. В этой конфигурации по умолчанию применяются оптимизации кода, поэтому он работает быстрее. Если JIT-компилятор увидит, что мы сортируем массив, но ничего не возвращает, он может понять это, и не вычислять значение переменное, которое в будущем нигде не используется. Поэтому я копирую массив (чтобы он не оказался потом отсортированным в памяти), и каждый раз сортирую его заново.

Чтобы убедиться, что копирование массива не влияет на результат, я сделал дополнительный бенчмарк для копирования массива:

-3

Итоговый класс получился вот таким:

-4

Часть 2. Определение IComparer-ов

У меня применяются 3 Comparer для строк. Первый я написал вот для предыдущей статьи (тут я назвал его MyStringComparer), второй — сделан с помощью Win32API (Win32Comparer), а третий взят напрямую из nuget-пакета. Давайте взглянем на их код:

MyStringComparer
MyStringComparer
Win32Comparer
Win32Comparer

NaturalStringComparer не влезет на скрин, его исходный код лежит здесь

Часть 3. Настройка Program.cs и .csproj

Файл Program.cs получился очень простым:

Console.ReadLine() чтобы консоль в конце не закрылась автоматически
Console.ReadLine() чтобы консоль в конце не закрылась автоматически

Для того, чтобы код выполнился в 2 версиях .NET, нам надо нажать правой кнопкой мыши на проект в обозревателе решений — Изменит файл проекта:

-8

И указать 2 TargetFrameworks:

-9

Запуск

Всё готово, приложение можно запускать. Не забудьте установить конфигурацию Release и запускайте приложение без отладки, чтобы все оптимизации применились:

-10

Всё скомпилировалось и запустилось, смотрим результаты:

-11

Что ж, давайте посмотрим на картинку и сделаем выводы:

  • Время для копирования массива составляет очень маленькую часть от времени сортировки и не особо влияет на результат.
  • За счёт оптимизаций среды выполнения в .NET 8 программа выполняется намного быстрее. То есть в Revit 2025 плагины должны работать быстрее.
  • Сортировке 2 способом (через Win32) практически всё равно, в какой среде мы её выполняем — на неё оптимизации не действуют. Причина этого в том, что мы вызываем исходный код Windows при импорте сборке, и по сути в обоих случаях выполняется один и тот же код WIndows (а для наших сортировок он разный, потому что среды выполнения компилируют его по разному). За счёт этого в .NET 4.8 этот код быстрейший, а в .NET 8 он уже уступил всем. Так что если пишите для Revit 2020 — 2024, смело используйте Comparer из Win32 — он и быстрее, и лучше сравнивает (относительно моего самописного)
  • Мой самописный Comparer показал чуть более быстрые результаты относительно nuget-Comparer в .Net 4.8, но уступил в .NET 8. Я думаю, это связано с использованием ReadOnlySpan, и в .NET 8 они лучше оптимизированы. Впрочем, в обеих средах мой Comparer проигрывает по памяти
  • Ну и самое главное — разница во времени выполнения в абсолютном времени для всех 3 вариантах настолько мала, что в принципе всё равно, чем пользоваться. Я бы советовал брать WIn32Comparer — он не добавляет дополнительных библиотек в итоговую сборку и даёт простое и удобное сравнение "прямо как в проводнике" без лишних заморочек и дописываний.

Заключение

Итоговый код вы можете посмотреть здесь. Вы можете клонировать репозиторий, запустить код у вас и посмотреть результаты на вашем компьютере. А ещё вы теперь знаете, как пользоваться бенчмарками и где найти справку по ним.

Не забывайте подписываться на мой телеграм-канал и мой GitHub, ставить лайки статьям и звёздочки репозиториям. До новых встреч!

-12