Mam taką klasę:
public class MyClass
{
public int Value { get; set; }
public bool IsValid { get; set; }
}
W rzeczywistości jest znacznie większy, ale to odtwarza problem (dziwność).
Chcę uzyskać sumę Value
, gdzie instancja jest prawidłowa. Jak dotąd znalazłem dwa rozwiązania tego problemu.
Pierwsza z nich to:
int result = myCollection.Where(mc => mc.IsValid).Select(mc => mc.Value).Sum();
Drugi to jednak:
int result = myCollection.Select(mc => mc.IsValid ? mc.Value : 0).Sum();
Chcę uzyskać najbardziej wydajną metodę. Na początku myślałem, że ten drugi będzie bardziej wydajny. Wtedy część teoretyczna mnie zaczęła mówić "No cóż, jedna to O (n + m + m), druga to O (n + n). Pierwsza powinna działać lepiej z większą liczbą inwalidów, a druga powinna działać lepiej mniej ”. Pomyślałem, że będą działać równie dobrze. EDYCJA: A potem @Martin wskazał, że Gdzie i Wybierz zostały połączone, więc powinno to być O (m + n). Jeśli jednak spojrzysz poniżej, wygląda na to, że nie ma to związku.
Więc wystawiłem to na próbę.
(To ponad 100 wierszy, więc pomyślałem, że lepiej będzie opublikować to jako streszczenie)
. Wyniki były ... interesujące.
Z 0% tolerancją krawata:
Skale są na korzyść Select
i Where
, o około ~ 30 punktów.
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where + Select: 65
Select: 36
Z 2% tolerancją krawata:
To jest to samo, z wyjątkiem tego, że dla niektórych były w granicach 2%. Powiedziałbym, że to minimalny margines błędu. Select
a Where
teraz mają tylko ~ 20 punktów przewagi.
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 6
Where + Select: 58
Select: 37
Z 5% tolerancją krawata:
To właśnie powiedziałbym, że jest to mój maksymalny margines błędu. To sprawia, że jest trochę lepiej Select
, ale niewiele.
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 17
Where + Select: 53
Select: 31
Z 10% tolerancją krawata:
To jest poza moim marginesem błędu, ale nadal jestem zainteresowany wynikiem. Bo to daje Select
i Where
punkt dwadzieścia prowadzić to miałem na chwilę teraz.
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 36
Where + Select: 44
Select: 21
Z 25% tolerancją krawata:
To jest sposób, aby wyjść poza mój margines błędu, ale nadal jestem zainteresowany wynikiem, ponieważ Select
i Where
nadal (prawie) utrzymują swoją 20-punktową przewagę. Wygląda na to, że deklasuje go w kilku wyraźnych i to właśnie daje mu przewagę.
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where + Select: 16
Select: 0
Zgaduję, że 20-punktowa przewaga nadeszła ze środka, gdzie obaj będą musieli obejść ten sam występ. Mógłbym spróbować to zarejestrować, ale byłoby to całe mnóstwo informacji do zebrania. Wykres byłby lepszy, jak sądzę.
Więc to właśnie zrobiłem.
Pokazuje, że Select
linia pozostaje stabilna (oczekiwana) i że Select + Where
linia wznosi się (oczekiwana). Zastanawiające mnie jednak, dlaczego nie spotyka się z Select
50 lub wcześniej: tak naprawdę spodziewałem się wcześniej niż 50, ponieważ trzeba było stworzyć dodatkowy wyliczający dla Select
i Where
. To znaczy, to pokazuje 20-punktową przewagę, ale nie wyjaśnia dlaczego. To chyba jest główny punkt mojego pytania.
Dlaczego tak się zachowuje? Powinienem mu ufać? Jeśli nie, powinienem użyć drugiego czy tego?
Jak @KingKong wspomniał w komentarzach, możesz również użyć Sum
przeciążenia, które pobiera lambdę. Więc moje dwie opcje są teraz zmienione na:
Pierwszy:
int result = myCollection.Where(mc => mc.IsValid).Sum(mc => mc.Value);
Druga:
int result = myCollection.Sum(mc => mc.IsValid ? mc.Value : 0);
Mam zamiar to trochę skrócić, ale:
How much do you want to be the disambiguation percentage?
0
Starting benchmarking.
Ties: 0
Where: 60
Sum: 41
How much do you want to be the disambiguation percentage?
2
Starting benchmarking.
Ties: 8
Where: 55
Sum: 38
How much do you want to be the disambiguation percentage?
5
Starting benchmarking.
Ties: 21
Where: 49
Sum: 31
How much do you want to be the disambiguation percentage?
10
Starting benchmarking.
Ties: 39
Where: 41
Sum: 21
How much do you want to be the disambiguation percentage?
25
Starting benchmarking.
Ties: 85
Where: 16
Sum: 0
Dwudziestopunktowa przewaga wciąż istnieje, co oznacza, że nie ma nic wspólnego z kombinacją Where
i Select
wskazaną przez @Marcin w komentarzach.
Dziękuję za przeczytanie mojej ściany tekstu! Ponadto, jeśli jesteś zainteresowany, oto zmodyfikowana wersja, która rejestruje plik CSV pobierany przez program Excel.
mc.Value
niej.Where
+Select
nie powoduje dwóch oddzielnych iteracji po kolekcji danych wejściowych. LINQ to Objects optymalizują go w jednej iteracji. Przeczytaj więcej na moim bloguOdpowiedzi:
Select
wykonuje iterację raz po całym zestawie i dla każdego elementu wykonuje gałąź warunkową (sprawdzanie poprawności) i+
operację.Where+Select
tworzy iterator, który pomija nieprawidłowe elementy (czy nieyield
), wykonując+
tylko na prawidłowych elementach.Tak więc koszt za
Select
to:I dla
Where+Select
:Gdzie:
p(valid)
to prawdopodobieństwo, że pozycja na liście jest ważna.cost(check valid)
jest kosztem oddziału, który sprawdza ważnośćcost(yield)
to koszt skonstruowania nowego stanuwhere
iteratora, który jest bardziej złożony niż prosty iteratorSelect
używany w wersji.Jak widać, dla danej
n
TheSelect
wersja jest na stałym poziomie, podczas gdyWhere+Select
wersja jest równanie liniowe zp(valid)
jako zmienną. Rzeczywiste wartości kosztów określają punkt przecięcia dwóch linii, a ponieważcost(yield)
mogą być różne odcost(+)
, niekoniecznie przecinają się przyp(valid)
= 0,5.źródło
cost(append)
? Naprawdę dobra odpowiedź, ale patrzy na to z innej perspektywy, a nie tylko statystyk.Where
nic nie tworzy, po prostu zwraca jeden element na raz zsource
sekwencji, jeśli tylko wypełnia predykat.IEnumerable.Select(IEnumerable, Func)
aIQueryable.Select(IQueryable, Expression<Func>)
. Masz rację, że LINQ nie robi „nic”, dopóki nie wykonasz iteracji po kolekcji, co prawdopodobnie miałeś na myśli.Oto szczegółowe wyjaśnienie, co powoduje różnice w czasie.
Sum()
FunkcjaIEnumerable<int>
wygląda tak:W C #
foreach
jest to po prostu cukier składniowy dla iteratora w wersji .Net (nie należy go mylić ) . Więc powyższy kod jest właściwie przetłumaczony na to:IEnumerator<T>
IEnumerable<T>
Pamiętaj, że porównujesz dwa wiersze kodu
Oto kicker:
LINQ używa odroczonego wykonania . Tak więc, chociaż może się wydawać, że
result1
iteruje po kolekcji dwukrotnie, w rzeczywistości dokonuje iteracji tylko raz.Where()
Warunek jest faktycznie stosowane w czasieSum()
, wewnątrz wywołaniaMoveNext()
(Jest to możliwe dzięki magiiyield return
) .Oznacza to, że dla
result1
kodu wewnątrzwhile
pętlijest wykonywany tylko raz dla każdego elementu z
mc.IsValid == true
. Dla porównaniaresult2
wykona ten kod dla każdego elementu w kolekcji. Dlategoresult1
generalnie jest szybszy.(Należy jednak pamiętać, że wywołanie
Where()
warunku w ramachMoveNext()
nadal wiąże się z niewielkim narzutem, więc jeśli większość / wszystkie elementy mająmc.IsValid == true
,result2
będzie faktycznie szybsze!)Miejmy nadzieję, że teraz jest jasne, dlaczego
result2
zwykle jest wolniejsze. Teraz chciałbym wyjaśnić, dlaczego podałem to w komentarzach te porównania wydajności LINQ nie mają znaczenia .Tworzenie wyrażenia LINQ jest tanie. Wywoływanie funkcji delegatów jest tanie. Przydzielanie i zapętlanie iteratora jest tanie. Ale jeszcze taniej jest nie robić tych rzeczy. Tak więc, jeśli okaże się, że instrukcja LINQ jest wąskim gardłem w programie, z mojego doświadczenia wynika, że przepisanie jej bez LINQ spowoduje zawsze sprawi, że będzie ona szybsza niż dowolna z różnych metod LINQ.
Tak więc przepływ pracy LINQ powinien wyglądać następująco:
Na szczęście wąskie gardła LINQ są rzadkie. Heck, wąskie gardła są rzadkie. Napisałem setki instrukcji LINQ w ciągu ostatnich kilku lat i ostatecznie zastąpiłem <1%. A większość z nich była spowodowana LINQ2EF słabą optymalizacją SQL , a nie winą LINQ.
Tak więc, jak zawsze, najpierw napisz przejrzysty i rozsądny kod i zaczekaj, aż po profilowaniu, aby martwić się o mikro-optymalizacje.
źródło
Zabawna rzecz. Czy wiesz, jak
Sum(this IEnumerable<TSource> source, Func<TSource, int> selector)
zdefiniowano? UżywaSelect
metody!Więc właściwie to wszystko powinno działać prawie tak samo. Sam przeprowadziłem szybkie poszukiwania i oto wyniki:
Do następujących realizacji:
mod
oznacza: każda 1 zmod
pozycji jest nieważna:mod == 1
każda pozycja jest nieważna, dlamod == 2
pozycji nieparzystych są nieważne itp. Kolekcja zawiera10000000
pozycje.Wyniki dla kolekcji z
100000000
elementami:Jak widać,
Select
iSum
wyniki są dość spójne we wszystkichmod
wartości. Jednakwhere
iwhere
+select
nie.źródło
Domyślam się, że wersja z Where odfiltrowuje 0 i nie są one tematem dla Sum (tj. Nie wykonujesz dodawania). To oczywiście domysł, ponieważ nie potrafię wyjaśnić, w jaki sposób wykonanie dodatkowego wyrażenia lambda i wywołanie wielu metod przewyższa zwykłe dodanie 0.
Mój przyjaciel zasugerował, że fakt, że 0 w sumie może spowodować poważny spadek wydajności z powodu kontroli przepełnienia. Byłoby interesujące zobaczyć, jak będzie to działać w niekontrolowanym kontekście.
źródło
unchecked
sprawiają, że jest to malutkie, odrobinę lepsze dlaSelect
.Analizując poniższy przykład, staje się dla mnie jasne, że jedyny przypadek, w którym + Select może przewyższać Select, to w rzeczywistości odrzucanie dużej ilości (około połowy w moich nieformalnych testach) potencjalnych pozycji z listy. W małym przykładzie poniżej otrzymuję z grubsza te same liczby z obu próbek, gdy Gdzie pomija około 4 mil z 10 mil. Uruchomiłem w wersji Release i zmieniłem kolejność wykonania where + select vs select z tymi samymi wynikami.
źródło
Select
?Jeśli potrzebujesz szybkości, prawdopodobnie najlepszym rozwiązaniem będzie wykonanie prostej pętli. A robienie
for
jest lepsze niżforeach
(zakładając oczywiście, że twoja kolekcja jest dostępna losowo).Oto czasy, które otrzymałem z 10% nieprawidłowych elementów:
A przy 90% nieprawidłowych elementów:
A oto mój kod porównawczy ...
Przy okazji, zgadzam się z przypuszczeniem Stilgara : względne prędkości w twoich dwóch przypadkach różnią się w zależności od odsetka nieważnych pozycji, po prostu dlatego, że ilość pracy, którą
Sum
trzeba wykonać, różni się w przypadku „Gdzie”.źródło
Zamiast próbować wyjaśnić za pomocą opisu, zamierzam przyjąć bardziej matematyczne podejście.
Biorąc pod uwagę poniższy kod, który powinien przybliżać to, co LINQ robi wewnętrznie, koszty względne są następujące:
Wybierz tylko:
Nd + Na
Gdzie + Wybierz:
Nd + Md + Ma
Aby ustalić punkt, w którym się skrzyżują, musimy zrobić małą algebrę:
Nd + Md + Ma = Nd + Na => M(d + a) = Na => (M/N) = a/(d+a)
Oznacza to, że aby punkt przegięcia miał wartość 50%, koszt wywołania delegata musi być mniej więcej taki sam, jak koszt dodania. Ponieważ wiemy, że rzeczywisty punkt przegięcia wynosił około 60%, możemy pracować wstecz i ustalić, że koszt wywołania delegata dla @ It'sNotALie był w rzeczywistości około 2/3 kosztu dodania, co jest zaskakujące, ale to właśnie mówią jego liczby.
źródło
Myślę, że to ciekawe, że wynik MarcinaJuraszka różni się od wyniku It'sNotALie. W szczególności wyniki Marcina Juraszka zaczynają się od wszystkich czterech wdrożeń w tym samym miejscu, podczas gdy wyniki It'sNotALie przecinają się wokół środka. Wyjaśnię, jak to działa ze źródła.
Załóżmy, że istnieją
n
wszystkie elementy im
prawidłowe elementy.Sum
Funkcja jest dość prosta. Po prostu przechodzi przez moduł wyliczający: http://typedescriptor.net/browse/members/367300-System.Linq.Enumerable.Sum(IEnumerable%601)Dla uproszczenia załóżmy, że kolekcja jest listą. Zarówno Select, jak i WhereSelect utworzą plik
WhereSelectListIterator
. Oznacza to, że faktycznie wygenerowane iteratory są takie same. W obu przypadkach istniejeSum
pętla nad iteratorem, czyliWhereSelectListIterator
. Najbardziej interesującą częścią iteratora jest MoveNext metoda .Ponieważ iteratory są takie same, pętle są takie same. Jedyną różnicą jest korpus pętli.
Ciało tych lambd ma bardzo podobny koszt. Klauzula where zwraca wartość pola, a predykat trójskładnikowy również zwraca wartość pola. Klauzula select zwraca wartość pola, a dwie gałęzie operatora trójskładnikowego zwracają wartość pola lub stałą. Połączona klauzula select ma gałąź jako operator trójskładnikowy, ale WhereSelect używa gałęzi w
MoveNext
.Jednak wszystkie te operacje są dość tanie. Najdroższą jak dotąd operacją jest branża, w której błędna prognoza będzie nas kosztować.
Inną kosztowną operacją jest tutaj
Invoke
. Wywołanie funkcji zajmuje trochę więcej czasu niż dodanie wartości, jak pokazał Branko Dimitrijevic.Ważenie jest również sprawdzaną akumulacją
Sum
. Jeśli procesor nie ma arytmetycznej flagi przepełnienia, sprawdzenie tego również może być kosztowne.Stąd interesujące koszty to: to:
n
+m
) * Wywołaj +m
*checked+=
n
* Wywołaj +n
*checked+=
Tak więc, jeśli koszt Invoke jest znacznie wyższy niż koszt sprawdzonej akumulacji, wtedy przypadek 2 jest zawsze lepszy. Jeśli są równe, równowagę zobaczymy, gdy około połowa elementów będzie poprawna.
Wygląda na to, że na systemie Marcina Juraszka, zaznaczone + = ma znikomy koszt, ale na systemach It'sNotALie i Branko Dimitrijevic, zaznaczone + = ma spore koszty. Wygląda na to, że jest najdroższy w systemie It'sNotALie, ponieważ próg rentowności jest znacznie wyższy. Nie wygląda na to, aby ktokolwiek opublikował wyniki z systemu, w którym akumulacja kosztuje znacznie więcej niż Invoke.
źródło