Niedawno zacząłem używać LINQ całkiem sporo i tak naprawdę nie widziałem żadnej wzmianki o złożoności czasu wykonywania żadnej z metod LINQ. Oczywiście w grę wchodzi wiele czynników, więc ograniczmy dyskusję do zwykłego IEnumerable
dostawcy LINQ-to-Objects. Dalej, załóżmy, że każda Func
przekazana jako selektor / mutator / itp. Jest tanią operacją O (1).
Jest oczywiste, że wszystkie te operacje są jedno-przepływowe ( Select
, Where
, Count
, Take/Skip
, Any/All
, etc.), można O (N), ponieważ wystarczy chodzić sekwencję raz; chociaż nawet to podlega lenistwu.
W przypadku bardziej złożonych operacji sytuacja jest bardziej mętna; zestaw podobny operatorów ( Union
, Distinct
, Except
, itd.), praca przy użyciu GetHashCode
domyślnie (AFAIK), więc wydaje się rozsądne, aby założyć, że używasz hash-table wewnętrznie, co czyni te operacje O (n), jak również, w ogóle. A co z wersjami, które używają IEqualityComparer
?
OrderBy
potrzebowałby sortowania, więc najprawdopodobniej patrzymy na O (n log n). A jeśli jest już posortowane? A jeśli powiem OrderBy().ThenBy()
i podam ten sam klucz do obu?
Widziałem GroupBy
(i Join
) używając sortowania lub mieszania. Który to jest?
Contains
byłoby O (n) na a List
, ale O (1) na a HashSet
- czy LINQ sprawdza podstawowy kontener, aby sprawdzić, czy może przyspieszyć działanie?
I prawdziwe pytanie - do tej pory wierzyłem, że operacje są skuteczne. Czy mogę jednak na to liczyć? Na przykład kontenery STL jasno określają złożoność każdej operacji. Czy istnieją podobne gwarancje wydajności LINQ w specyfikacji biblioteki .NET?
Więcej pytań (w odpowiedzi na komentarze): Tak
naprawdę nie myślałem o narzutach, ale nie spodziewałem się, że będzie wiele dla prostych Linq-to-Objects. W poście CodingHorror jest mowa o Linq-to-SQL, w którym rozumiem, że analizowanie zapytania i tworzenie SQL zwiększyłoby koszty - czy dostawca obiektów ma podobny koszt? Jeśli tak, czy jest inaczej, jeśli używasz składni deklaratywnej lub funkcjonalnej?
Odpowiedzi:
Gwarancji jest bardzo, bardzo niewiele, ale jest kilka optymalizacji:
Rozszerzenie metod, które używają indeksowanego dostępu, takie jak
ElementAt
,Skip
,Last
lubLastOrDefault
, sprawdzi, czy zaistniałych typu narzędziIList<T>
, aby uzyskać O (1) Dostęp zamiast O (n).Do
Count
kontroli sposobu doICollection
realizacji tak, że ta operacja jest O (1) zamiast O (n).Distinct
,GroupBy
Join
I wierzę również sposoby zestaw agregacji (Union
,Intersect
iExcept
) stosowanie hashowania, więc powinny być zbliżone do O (N) zamiast O (N²).Contains
sprawdzaICollection
implementację, więc może to być O (1), jeśli bazowa kolekcja jest również O (1), na przykład aHashSet<T>
, ale zależy to od faktycznej struktury danych i nie jest gwarantowane. Hash sety przesłaniająContains
metodę, dlatego mają wartość O (1).OrderBy
metody używają stabilnego szybkiego sortowania, więc są średnim przypadkiem O (N log N).Myślę, że obejmuje to większość, jeśli nie wszystkie, wbudowane metody rozszerzające. Naprawdę jest bardzo niewiele gwarancji wydajności; Sam Linq spróbuje wykorzystać wydajne struktury danych, ale nie jest to wolny przebieg do napisania potencjalnie nieefektywnego kodu.
źródło
IEqualityComparer
przeciążeniami?IEqualityComparer
, nie mogę uzasadnić, by wpływał na asymptotyczną złożoność.EqualityComparer
narzędziGetHashCode
tak dobrzeEquals
; ale oczywiście ma to sens.Orderby().ThenBy()
nadalN logN
czy jest(N logN) ^2
czy coś takiego?Od dawna wiem, że
.Count()
zwraca,.Count
jeśli wyliczenie toIList
.Ale zawsze byłem nieco zmęczony o złożoności czasu wykonywania operacji Set:
.Intersect()
,.Except()
,.Union()
.Oto dekompilowana implementacja BCL (.NET 4.0 / 4.5) dla
.Intersect()
(moje komentarze):Wnioski:
IEqualityComparer<T>
również musi pasować).Aby uzyskać kompletność, oto implementacje dla
.Union()
i.Except()
.Uwaga spoiler: one również mają złożoność O (N + M) .
źródło
Wszystko, na co możesz naprawdę liczyć, to to, że metody Enumerable są dobrze napisane dla ogólnego przypadku i nie będą używać naiwnych algorytmów. Prawdopodobnie istnieją materiały osób trzecich (blogi itp.), Które opisują faktycznie używane algorytmy, ale nie są one oficjalne ani gwarantowane w takim sensie, jak algorytmy STL.
Aby to zilustrować, oto odzwierciedlony kod źródłowy (dzięki uprzejmości ILSpy)
Enumerable.Count
z System.Core:Jak widać, trzeba się postarać, aby uniknąć naiwnego rozwiązania polegającego na prostym wyliczeniu każdego elementu.
źródło
Enumerable.Count
nie jest iterowana, chyba że nie ma oczywistej alternatywy. Jak mógłbyś uczynić to mniej naiwnym?Właśnie wyłamałem reflektor i sprawdzają podstawowy typ, kiedy
Contains
zostanie wywołany.źródło
Prawidłowa odpowiedź brzmi „to zależy”. zależy to od typu bazowego IEnumerable. Wiem, że w przypadku niektórych kolekcji (takich jak kolekcje, które implementują ICollection lub IList) są używane specjalne ścieżki kodowe, jednak rzeczywista implementacja nie gwarantuje nic specjalnego. na przykład wiem, że ElementAt () ma specjalny przypadek dla indeksowanych kolekcji, podobnie jak Count (). Ale generalnie powinieneś prawdopodobnie założyć wydajność O (n) w najgorszym przypadku.
Ogólnie rzecz biorąc, nie sądzę, abyś znalazł taki rodzaj gwarancji wydajności, jaki chcesz, ale jeśli napotkasz konkretny problem z wydajnością z operatorem linq, zawsze możesz go ponownie zaimplementować do swojej konkretnej kolekcji. Istnieje również wiele blogów i projektów rozszerzających, które rozszerzają Linq na Objects w celu dodania tego rodzaju gwarancji wydajności. sprawdź indeksowane LINQ, które rozszerza i dodaje do zestawu operatora, aby uzyskać więcej korzyści związanych z wydajnością.
źródło