Benchmarki
Postanowiliśmy zanalizować najczęstsze sytuacje – uruchomienie opisanych wcześniej metod na liście oraz na tablicy. Oprócz tego dla porównania sprawdzone zostały właściwości Count
(nie mylić z metodą Count()
) oraz Length
. Benchmarki uruchomiono w różnych środowiskach: .NET 5.0, .NET Core 3.1 oraz .Net Framework 4.8 dla danych o wielkościach 10, 1000 i 1000000.
Pierwszą dobrą wiadomością jest to, że wszystkie implementacje mają złożoność O(1)
, wykonując się w takim samym czasie niezależnie od rozmiaru danych. Oznacza to, że skróty, które widzieliśmy, faktycznie działają.
Drugim bardzo ważnym spostrzeżeniem jest to, że property Count
oraz Length
wykonały się w praktycznie zerowym czasie. Zasadniczo jest to po prostu odwołanie się do odpowiedniego miejsca w pamięci. Przyspieszenie tej operacji jest praktycznie niemożliwe. Stąd pierwszy bardzo ważny wniosek – gdy tylko możemy, używajmy pól Count
oraz Length
.
Trzecią istotną rzeczą jest fakt, że wszystkie metody uruchomione w .NET 5.0 nie wykonują ani jednej alokacji, co potwierdza, że i tam skróty działają dobrze.
Przyjrzyjmy się teraz konkretnym wynikom.
List.Count()
vs List.Any()
Pierwszym spostrzeżeniem w porównaniu metod Count()
oraz Any()
jest fakt, że w każdym środowisku, poza najnowszym, ta pierwsza wypada zdecydowanie lepiej. Może to być zaskoczeniem dla osób, które obstawiały, że policzenie czy jakikolwiek element istnieje jest szybsze, niż policzenie wszystkich elementów i sprawdzenie czy jest ich więcej niż 0. Bierze się to z faktu wykorzystanych skrótów oraz braku alokacji w implementacji Count()
– kod skacze prawie od razu do właściwości Count
, która na bieżąco kontroluje zawartość listy. W przypadku najnowszego środowiska różnica jest w granicach błędu i nie ma znaczenia, której metody użyjemy.
Można również zauważyć różnicę między Count()
w .NET Core 3.1 a 5.0 na niekorzyść nowego, co jest bardzo zaskakującą sprawą, której nie jestem w stanie teraz wytłumaczyć. Dla pewności powtórzyłem benchmark parę razy i faktycznie ta różnica zawsze występuje. Jeśli znajdę przyczynę tego problemu na pewno uzupełnię ten tekst.
Array.Count()
vs Array.Any()
Wyniki są zupełnie inne, niż dla listy. Any()
jest szybsze w każdym środowisku, jedynie w najnowszym różnica jest na krawędzi błędu. Co więcej – Any()
jest szybsze mimo tego, że wykonuje alokacje w .NET Framework i .NET Core 3.1.
Bierze się to z tego, że tablica, aby pobrać długość wywołuje poprzez SZArrayHelper
. Jest to spowodowane faktem, że z punktu widzenia języka tablica implementuje interfejs IEnumerable
, natomiast w rzeczywistości tablica tej implementacji nie posiada. W ten sposób nieco oszukujemy wirtualną maszynę uruchamiającą kod. Więcej informacji na ten temat można znaleźć pod tym linkiem: c# – Purpose of TypeDependencyAttribute(„System.SZArrayHelper”) for IList<T>, IEnumerable<T> and ICollection<T>? – Stack Overflow
List.Count()
vs Array.Any()
W poprzednich akapitach odkryliśmy, że nie ma prostego przepisu „zawsze używaj Count()
” albo „zawsze używaj Any()
„. Wszystko zależy od konkretnego przypadku. Dlatego teraz porównamy najszybsze spośród metod – List.Count()
oraz Array.Any()
Być może nie jest to zaskoczeniem, że List.Count()
wypadło zdecydowanie lepiej, będąc zawsze przynajmniej dwukrotnie szybsze od rywala. Powód jest prosty – List.Count()
prawie od razu przeskakuje do pola Count
, w trakcie, gdy Array.Any()
musi korzystać z bardziej skomplikowanych operacji (a do tego w starszych wersjach .NET wykonywać alokacje).
Castowanie na IEnumerable
Na zdjęciu pełnych wyników widać jeszcze inne, nieomówione testy: ListAsEnumerableCount
, ListAsEnumerableAny
, ArrayAsEnumerableCount
oraz ArrayAsEnumerableAny
. Polegały one na castowaniu listy i tablicy na IEnumerable
, a następnie uruchamianie odpowiednich metod. Nie wpłynęło to w żadnym stopniu na wynik a wszystkie różnice mieszczą się w granicach błędu, czego można było się spodziewać.