Wydajność sprawdzania zawartości kolekcji

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.

Wyniki benchmarków. Kliknij aby powiększyć.

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()

Wyniki testu List.Count() oraz List.Any() w różnych środowiskach

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 testu Array.Count() oraz Array.Any() w różnych środowiskach

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()

Wyniki testu 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ć.

Wywołanie metody Count() na liście oraz na liście zcastowanej na IEnumerable