Працюючи в компанії Perfectial, я займаюсь розподіленою системою з мікросервісною архітектурою, яка має працювати при високих навантаженнях. Найпростішим методом витримувати навантаження є масштабування, але ми намагаємось максимально оптимізувати продуктивність компонентів системи, а масштабування використовувати лише при пікових навантаженнях. Тому для нас важливо знати наскільки швидко працюють компоненти системи.
Profiling vs Benchmarking
Коли виникає проблема з продуктивністю коду, на думку зазвичай спадає запустити профайлер та зробити заміри виконання.
WIKI: Профілювання — збір та аналіз інформації про виконання програми з метою оптимізації її роботи.
Зазвичай процес оптимізації продуктивності відбувається так:
- Запускаємо профайлер, визначаємо повільні точки виконання,
- Вносимо зміни для оптимізації продуктивності в проблематичних точках,
- Запускаємо профайлер для того щоб переконатись чи позбулись ми точок з низькою швидкодією.
В наведеному вище прикладі може бути складно пересвідчитись чи став код швидшим, чи дійсно знайдену проблему вдалося вирішити, водночас не створивши нових, оскільки профайлер не дає точного часу виконання. Призначення профайлера – визначити, які блоки коду виконувались, наскільки часто, та скільки часу витрачено на їх виконання в загальному.
Для того, щоб переконатись, що зроблені зміни насправді пришвидшили продуктивність найкраще використовувати benchmark-тести.
WIKI: Тест продуктивності, або бенчмарк (англ. benchmark) — контрольне завдання, необхідне для визначення порівняльних характеристик продуктивності комп'ютерної системи.
Використовувати benchmark-тести можна для порівняння продуктивності:
- різних версій коду (до і після певних змін),
- різних модулів (порівняння різних алгоритмів для вирішення проблеми),
- продуктивності на різному апаратному забезпеченні або середовищі виконання.
З використанням benchmark-тестів процес оптимізації продуктивності може відбуватись наступним чином:
- Запускаємо профайлер, визначаємо повільні точки виконання,
- Запускаємо benchmark тести, робимо заміри швидкодії,
- Вносимо зміни для оптимізації продуктивності в проблематичних точках,
- Знову запускаємо 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 виконує наступні кроки:
- BenchmarkRunner генерує ізольовані проекти для кожного benchmark-тесту та компілює їх в Release режимі;
- Бере кожен benchmark-тест та пробує заміряти його продуктивність запускаючи benchmark процес декілька разів;
- Після всіх замірів, 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-тести, і вже в ході роботи набувати нових, глибших знань.
Корисні посилання:
- BenchmarkDotNet: http://benchmarkdotnet.org/
- Andrey Akinshin, блог: http://aakinshin.net/
- Adam Sitnik, блог: http://adamsitnik.com/
Ще немає коментарів