Mierzenie Powiedzmy, że chcesz zmierzyć prędkość, ilość zajmowanej pamięci jednego bloku kodu w kontekście drugiego.
Chcesz zrobić porównanie i ustalić, który kod jest lepszy.
W C# istnieje już gotowe do narzędzie i przy okazji możesz też zmierzyć jak szybko dany fragment kodu działa w zależności od wersji .NET Frameworka.
SPOILER .NET 6 jest najszybszy
Na początku utwórz prosty projekt aplikacji konsolowej i zainstaluj paczkę NuGet "BenchmarkDotNet".
Pilnuj, aby projekt twojej aplikacji konsolowej był elastyczny. Czyli nie używaj najnowszych funkcjonalność z języka C#. Mówimy tutaj głównie o minimalnych aplikacjach
class Program
{
static void Main(string[] args)
{
var results = BenchmarkRunner.Run<Demo>();
}
}
Oto nasz pierwszy test Demo. Chce przetestować co jest szybsze: czy lista moich produktów i potem użycie metody First() albo Single() czy być może słownik, gdzie ID będzie kluczem.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
Ten prosty test wymaga użycia metody, która nam utworzy tę listę i słowniki i wiadomo nie chcemy, aby samo tworzenie tych kolekcji zaburzało mierzenie prędkości tych rozwiązań.
Nasza klasa demo więc będzie miała dwie kolekcję.
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net48)]
public class Demo
{
private List<Product> products;
private Dictionary<int,string> productDictionary;
Atrybut "MemoryDiagnoser" doda nam kolumny, które zmierzą nam użycie pamięci oraz samego GarbageCollectora.
Atrybut "SimpleJob" mówi nam, że chcemy te testy wykonać pod kontem ".Net 6.0" i ".Net frameworka 4.8". To jest właśnie powód, dla którego nie możemy do testu umieszczać kodu z C# 10 i 9.0, ponieważ w ".Net frameworka 4.8" można używać co najwyżej z C# 7.0 i nawet co do tego nie jestem pewien.
Jest też tutaj pewna ważna rzecz, która mi wywala czasem całe Visual Studio. Aby ten projekt mógłby testowany musisz zmienić atrybut "TargetFrameworks" w tym projekcie
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0;Net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
</ItemGroup>
</Project>
Mam więc dwie kolekcję do testu. Teraz utwórzmy metodę, która nam te kolekcje zapełni do testu. Musimy ją oznaczyć atrybutem "GlobalSetup"
[GlobalSetup]
public void Setup()
{
products = new List<Product>();
productDictionary = new Dictionary<int,string>();
for (int i = 1; i < 2001; i++)
{
products.Add(
new Product()
{
Id = i, Name = i.ToString()
});
productDictionary.Add(i, i.ToString());
}
}
Teraz do klasy Demo dorzucamy metody, które będą mierzone. Oznaczamy je atrybutem "Benchmark".
[Benchmark]
public string CheckLinqFirstonList()
{
return products.First(a => a.Id == 1000).Name;
}
[Benchmark(Baseline = true)]
public string CheckLinqSingleonList()
{
return products.Single(a => a.Id == 1000).Name;
}
[Benchmark]
public string CheckDictionary()
{
return productDictionary[1000];
}
Nasz test jest gotowy. Jak myślisz co będzie szybsze? Znalezienie elementu o ID 1000 przy użyciu metody First() czy Single() w kolekcji, która ma 2001 elementów.
Jak szybko zrobi przeszukanie sam słownik?
Zanim zaczniemy. Musimy ustawić uruchomienie naszego kodu w trybie "Release" , aby takie mierzenie miało sens.
W trybie "Debug" do kodu dodaje sie mnóstwo metadanych, aby no właśnie kod mógłby być Debugowany tylko to może zaburzyć wyniki naszego testu.
Wyniki są następujące
Słownik oczywiście jest najszybszy i to o jakieś 4000 razy od metody First().
Metoda First() jak znajdzie element to przestanie szukać więc przerywa swoje szukanie na elemencie 1000.
Natomiast metoda Single() jeszcze się upewnia czy szukany element na pewno jest pojedyńczy, a więc skanuje on całą kolekcję. Nasza lista ma 2001 elementów, a więc metoda Single w tym kontekście będzie dwa razy wolniejsza od metody First(), bo musi zeskanować drugie tyle elementów.
.NET 6 oczywiście jest szybszy od .NET Frameworka 4.8 i takie testy mogą pokazać o ile.
Jeśli chodzi o alokacje pamięci i użycie Garbage Collector to akurat ten test nic nie wykazuje. 40 bajtów to utworzenie napisu String, który jest zwracany w teście dla Single i dla First
Z tego, co wiem ta paczka NuGet nie oferuje innej formy sprawdzania prędkości kodu. Czyli jeśli chcesz zrobić kolejny test to tworzysz kolejny projekt konsolowy.
Oto przykłady innych moich testów:
class Program
{
static void Main(string[] args)
{
var results = BenchmarkRunner.Run<IsEvenBenchmark>();
Console.ReadKey();
}
}
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net472)]
public class IsEvenBenchmark
{
[Benchmark]
[Arguments(42)]
public bool IsEven(int i)
{
return i % 2 == 0;
}
}
Tutaj możemy wykazać, że nawet prosta operacja dzielenia modulo jest szybsza w .NET 6, bo kompilator generuje mniej kodu IL dla takiej operacji.
Możemy też sprawdzić jak szybciej działa metoda string.Join w .NET 6.
class Program
{
static void Main(string[] args)
{
var results = BenchmarkRunner.Run<JoinBenchmark>();
}
}
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net50)]
[SimpleJob(RuntimeMoniker.Net472)]
[MemoryDiagnoser]
public class JoinBenchmark
{
private List<string> _strings = new List<string>()
{ "Hi", "How", "are", "you", "today" };
[Benchmark]
public string Join()
{
return string.Join(", ", _strings);
}
}
Sprawdzić jak szybciej działają metody matematyczne w C# w .NET 6
class Program
{
static void Main(string[] args)
{
var results = BenchmarkRunner.Run<Demo>();
Console.ReadKey();
}
}
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net48)]
public class Demo
{
[Benchmark]
public double GetHeight() => GetHeight(20.0,
10.0, 8.0, 6.0);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double GetHeight(double longbase, double shortbase,
double leftLeg, double rightLeg)
{
double x = (Math.Pow(rightLeg, 2.0) - Math.Pow(leftLeg, 2.0) + Math.Pow(longbase, 2.0) + Math.Pow(shortbase, 2.0) - 2 * shortbase * longbase) / (2 * (longbase - shortbase));
return Math.Sqrt(Math.Pow(rightLeg, 2.0) - Math.Pow(x, 2.0));
}
}
Jak szybciej działają typy wyliczeniowe w .NET 6
class Program
{
static void Main(string[] args)
{
var results = BenchmarkRunner.Run<EnumBenchmark>();
}
}
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net50)]
[MemoryDiagnoser]
public class EnumBenchmark
{
private DayOfWeek _value = DayOfWeek.Friday;
[Benchmark]
public bool IsDefined()
{
return Enum.IsDefined(_value);
}
[Benchmark]
public string GetName()
{
return Enum.GetName(_value);
}
[Benchmark]
public string[] GetNames()
{
return Enum.GetNames<DayOfWeek>();
}
}
Oto mój prosty test, który sprawdza jak najlepiej jest budować adres URL w C#
class Program
{
static void Main(string[] args)
{
var results = BenchmarkRunner.Run<Demo>();
}
}
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net472)]
public class Demo
{
[Benchmark]
public string UrlBuildWithUriBuilder()
{
var builder = new UriBuilder();
builder.Scheme = "https";
builder.Host = "dotnet.microsoft.com";
builder.Port = 443;
builder.Path = "/platform/try-dotnet";
return builder.ToString();
}
[Benchmark]
public string UrlBuildWithStrings()
{
string scheme = "https";
string host = "dotnet.microsoft.com";
string port = "443";
string path = "/platform/try-dotnet";
return scheme + "//" + host + ":" + port + path;
}
}
Jak widzisz możliwości testowania jest wiele. Z paczką NuGet Benchmark.NET mam nadzieje, że twoja przygoda się dopiero zaczyna
Chcesz więcej to zapraszam na swój kanał YouTube: