Wydajność funkcji jest ważną miarą w każdej aplikacji. Zanim zaczniemy optymalizować kod aplikacji w pierwszej kolejności należy zidentyfikować fragment kodu, który działa nie optymalnie, tu z pomocą przyjdą profilery. Udało się znaleźliśmy fragment kodu, wprowadziliśmy poprawkę i jak najszybciej można porównać wydajność poprawki w odniesieniu do wersji pierwotnej? Pierwszym naszym odruchem jest wykorzystanie klasy StopWatch z przestrzeni nazw System.Diagnostics. Klasa jest „stoperem” udostępniająca metody służące do dokładnego pomiaru czasu wykonania danego fragmentu kodu. Inną opcją udostępniającą większe możliwości i przyspieszającą proces testowania jest biblioteka BenchmarkDotNet.

Testy z BenchmarkDotNet

Dziś wykorzystam VS Code, zatem zacznijmy od stworzenia projektu i dodania paczki BenchmarkDotNet z NuGet z poziomu .NET Core CLI.

W ramach testu zostaną porównane czasy wykonania dwóch metod, których zadaniem jest inicjalizacja dwuwymiarowej tablicy wartościami -9999 (nodata). Metody, których czasy wykonania chcemy porównać oznaczamy atrybutem Benchmark.

Po zaimplementowaniu naszej klasy, należy uruchomić benchmark. Benchmark możemy uruchomić na kilka sposobów: Types, Url, Source lub BenchmarkSwitcher. W analizowanym przykładzie uruchomię za pomocą Types.

Aplikacje uruchamiamy dla konfiguracji Release w celu uzyskania wyników zgodnych z rzeczywistością. W przypadku uruchomienia aplikacji w konfiguracji Debug otrzymamy ostrzeżenie. Przed wykonaniem testów, najlepiej zamknąć wszystkie aplikacje, także IDE i uruchomić aplikacje przez interpreter poleceń np. PowerShell.

Wyniki analizy

Benchmark zależy od maszyny, na każdej stacji roboczej wyniki mogą być inne. Po wykonaniu analizy porównawczej, otrzymamy wyniki w formie poniższej tabeli. Rezultaty analizy także zapisywane są w katalogu .\BenchmarkDotNet.Artifacts\results w naszym projekcie. Znajdziemy tam np. plik .csv, .md, .html z wynikami analizy (domyślne formaty).

Summary BenchmarkDotNet

Testowane metody różniły się sposobem przejścia po macierzy. Najlepszy wyniki uzyskano dla przypadku przejścia wierszami, który był wydajniejszy od przejścia kolumnami. Elementy tablicy ułożone są w ciągłym obszarze pamięci wierszami. Czytając kolumnami za każdym razem odwołujemy się do innego cache line (zazwyczaj 64 bajty), co nie jest w tym przypadku wydajne. Czytając wierszami wykorzystujemy cały cache line.

Fluent config

BenchmarkDotNet udostępnia możliwość własnej konfiguracji testów po przez implementacje klasy dziedziczącej po ManualConfig lub użycie fluent interface. W configu możemy ustawić następujące elementy:

  • Jobs – opisują środowisko testu (Platform, Runtime, Jit, GcMode), oraz sposób testowania metody (RunStrategy , LaunchCount, WarmupCount, TargetCount, IterationTime, UnrollFactor, InvocationCount). BenchmarkDotNet posiada algorytm automatycznego wybierania wartości parametrów testowania metody, który dopasowuje odpowiednią ilość wywołań metod;
  • Columns – określają jakie kolumny znajdą się w tabeli podsumowania testu;
  • Exporters – określają jakie formaty plików z wynikami będą znajdowały się w katalogu .\BenchmarkDotNet.Artifacts\results;
  • Loggers – umożliwiają logowanie wyników testów np. na console, do pliku;
  • Diagnosers – umożliwiają dodanie danych diagnostycznych do wyniku testu;
  • Analyzers – analizują benchmark i mogą tworzyć ostrzeżenia np. o ustawionym trybie debug;
  • Validators – weryfikują benchmark przed wykonaniem;
  • Filters – umożliwiają filtrowanie testów;
  • OrderProvider – umożliwia dostosowanie wyświetlania kolejności wyników w tabeli podsumowującej test.

Poniżej przykład konfiguracji z użyciem fluent interface.

Podsumowanie

BenchmarkDotNet umożliwia w prosty sposób wykonanie testów wydajności metod poprzez dodanie kilku atrybutów. Biblioteka ma duże możliwości konfiguracji, o których więcej można doczytaj w dokumentacji.