Замір продуктивності .NET коду з допомогою BenchmarkDotNet

8 хв. читання

Працюючи в компанії Perfectial, я займаюсь розподіленою системою з мікросервісною архітектурою, яка має працювати при високих навантаженнях. Найпростішим методом витримувати навантаження є масштабування, але ми намагаємось максимально оптимізувати продуктивність компонентів системи, а масштабування використовувати лише при пікових навантаженнях. Тому для нас важливо знати наскільки швидко працюють компоненти системи.

Profiling vs Benchmarking

Коли виникає проблема з продуктивністю коду, на думку зазвичай спадає запустити профайлер та зробити заміри виконання.

WIKI: Профілювання — збір та аналіз інформації про виконання програми з метою оптимізації її роботи.

Зазвичай процес оптимізації продуктивності відбувається так:

  1. Запускаємо профайлер, визначаємо повільні точки виконання,
  2. Вносимо зміни для оптимізації продуктивності в проблематичних точках,
  3. Запускаємо профайлер для того щоб переконатись чи позбулись ми точок з низькою швидкодією.

В наведеному вище прикладі може бути складно пересвідчитись чи став код швидшим, чи дійсно знайдену проблему вдалося вирішити, водночас не створивши нових, оскільки профайлер не дає точного часу виконання. Призначення профайлера – визначити, які блоки коду виконувались, наскільки часто, та скільки часу витрачено на їх виконання в загальному.

Для того, щоб переконатись, що зроблені зміни насправді пришвидшили продуктивність найкраще використовувати benchmark-тести.

WIKI: Тест продуктивності, або бенчмарк (англ. benchmark) — контрольне завдання, необхідне для визначення порівняльних характеристик продуктивності комп'ютерної системи.

Використовувати benchmark-тести можна для порівняння продуктивності:

  • різних версій коду (до і після певних змін),
  • різних модулів (порівняння різних алгоритмів для вирішення проблеми),
  • продуктивності на різному апаратному забезпеченні або середовищі виконання.

З використанням benchmark-тестів процес оптимізації продуктивності може відбуватись наступним чином:

  1. Запускаємо профайлер, визначаємо повільні точки виконання,
  2. Запускаємо benchmark тести, робимо заміри швидкодії,
  3. Вносимо зміни для оптимізації продуктивності в проблематичних точках,
  4. Знову запускаємо benchmark-тести, робимо заміри швидкодії, порівнюємо їх з попередніми – точно дізнаємось чи став код виконуватись швидше.

Коли слід починати думати про продуктивність

В процесі проектування системи та написання коду проводиться низка дій, які, в кінцевому результаті, здатні впливати на складність подальшої її оптимізації. Тому, перш за все, перед оптимізацією продуктивності:

  • Продумайте загальну архітектуру,
  • Оберіть швидкі алгоритми,
  • Оберіть оптимальні структури даних,
  • Оптимізуйте роботу з пам'яттю,
  • Оптимізуйте роботу з мережею,
  • Додайте кешування.

Можна відкласти вищевказані кроки до етапу оптимізації продуктивності, але в такому випадку необхідні зміни накопичаться, і вони будуть більш глобальними. Тому писати код потрібно постійно думаючи про продуктивність, а приймати важливі поточні рішення слід аналізуючи потенційні проблеми чи обмеження.

BenchmarkDotNet

BenchmarkDotNet – бібліотека для написання benchmark-тестів, яка допомагає уникати поширених помилок. Бібліотека набула широкої популярності, і, для прикладу, використовується в таких проектах як:

  • CoreCLR (.NET Core runtime)
  • CoreFX (.NET Core базова бібліотека класів),
  • Roslyn (компілятор для C# та Visual Basic)
  • KestrelHttpServer (кросплатформний веб-сервер для ASP.NET Core)
  • SignalR
  • EntityFrameworkCore
  • F#

Щоб побачити, наскільки простим є написання та запуск тестів, розглянемо два простих benchmark-тести для порівняння продуктивності двох блоків коду. Уявімо, що потрібно написати максимально продуктивне рішення для виділення цілого числа з рядка, а метод int.Parse для нас є недостатньо швидким. Відтак, візьмемо int.Parse для базового порівняння.

Для початку створюємо Console Application та інсталюємо компоненти BenchmarkDotNet:

  • Install-Package BenchmarkDotNet -Version 0.10.13

Додаємо тест і його запуск з методу Main:


using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace BenchmarkDotNet.Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<IntParseBenchmark>();
        }
    }

    public class IntParseBenchmark
    {
        private string integer = int.MaxValue.ToString();

        [Benchmark]
        public int Int_Parse()
        {
            return int.Parse(integer);
        }

        [Benchmark]
        public int CustomIntParse()
        {
            int val = 0;
            foreach (var c in integer) val = 10 * val + (c - '0');
            return val;
        }
    }
}

Запускати тести потрібно без налагоджувача (Ctrl + F5), інакше BenchmarkDotNet проінформує про помилку і не запускатиме тести. Запустивши проект в консолі побачимо детальну інформацію про процес запуску тестів, середовище виконання, апаратне забезпечення, і в кінці виконання отримаємо таблицю з результатами замірів:

benchmark-тест – функція з атрибутом [Benchmark], де тілом функції є блок коду, швидкодію якого ми хочемо заміряти.

Статистики

Після завершення процесу запуску тестів BenchmarkDotNet обраховує статистики для кожного тесту і виводить їх в консоль (також експортує їх у файли результатів) у вигляді таблиці, стовпцями якої є статистики. Найпопулярніші статистики, котрі будуть потрібні для початку роботи з BenchmarkDotNet:

  • Mean – арифметичне середнє всіх вимірювань
  • StandardError – стандартна похибка всіх вимірювань
  • StandardDeviation – стандартне відхилення всіх вимірювань
  • Error – довірчий інтервал з ймовірністю 99.9%
  • Min
  • Q1 – Квартиль 1 (25-тий процентиль)
  • Median (Q2) – Значення, що розташоване в середині ряду всіх вимірювань (50-тий процентиль)
  • Q3 – Квартиль 3 (75-тий процентиль)
  • Max
  • P0, P25, P50, P67, P80, P85, P90, P95, P100 – Процентилі

Набір статистик (колонок) в таблиці результатів можна сконфігурувати через ColumnsConfiguration.

Як працює BenchmarkRunner

BenchmarkRunner – вхідна точка для запуску тестів, з його допомогою можна запустити цілий набір тестів. Під час виконання тестів BenchmarkRunner виконує наступні кроки:

  1. BenchmarkRunner генерує ізольовані проекти для кожного benchmark-тесту та компілює їх в Release режимі;
  2. Бере кожен benchmark-тест та пробує заміряти його продуктивність запускаючи benchmark процес декілька разів;
  3. Після всіх замірів, BenchmarkDotNet створює:
  • Екземпляр класу Summary який містить всю інформацію про benchmark запуски.
  • Набір файлів що містять результати тестів в human-readable та machine-readable форматах.
  • Графіки результатів виконання тестів.

Як працює benchmark-тест

В термінології BenchmarkDotNet виклик benchmark-тесту називається операцією. Водночас, певна кількість операцій називається ітерацією. Запускаючи benchmark-тест BenchmarkDotNet виконує наступні типи ітерацій у вказаній послідовності:

  • Pilot: обирається оптимальна кількість операцій, що будуть запускатись в одній ітерації.
  • IdleWarmup, IdleTarget: вираховується час, який витрачається на запуск тесту (overhead).
  • MainWarmup: Прогрів benchmark-тесту.
  • MainTarget: заміри виконання тесту.
  • Result: вираховуються статистики (MainTarget – AverageOverhead).

Конфігурація

Конфігурація дозволяє повністю контролювати роботу BenchmarkDotNet, основними блоками конфігурації є:

  • Columns – описує статистики, які потрібно відображати в таблиці результатів.

  • Jobs – описує як повинні запускатись тести, дозволяє сконфігурувати середовище виконання (Platform: x86 or x64; Runtime: Clr, Core, Mono; Jit: LegacyJit, RyuJit, Llvm; GcMode), стратегію запуску (RunStrategy: Throughput, ColdStart, Monitoring) та інше.

  • Diagnosers – конфігурація діагностик для збирання додаткової інформації: MemoryDiagnoser – збирає інформацію про виділення пам'яті та роботу GC; Disassembly Diagnoser – дозволяє декомпілювати код в asm, IL, C#/F#; Hardware Counter Diagnoser – збирає метрики процесора (hardware performance counters); InliningDiagnoser; TailCallDiagnoser.

Підсумок

Важливо писати код який буде не просто працювати, а буде працювати швидко, тому писати benchmark-тести й відстежувати продуктивність так само важливо як писати unit-тести й відстежувати правильність виконання коду.

BenchmarkDotNet бібліотека дозволяє забути про більшість проблем пов'язаних з benchmark тестуванням, сама визначає як оптимально запускати тести щоб отримати максимально правильні результати й отримувати результати в максимально зручному форматі. Велика популярність BenchmarkDotNet обумовлена зручністю використання, простотою конфігурації, а написання benchmark-тестів по стилю дуже подібне до написання unit-тестів. Достатньо мати базові знання бібліотеки, щоб почати писати benchmark-тести, і вже в ході роботи набувати нових, глибших знань.

Корисні посилання:

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація