Czy kolejność funkcji LINQ ma znaczenie?

114

Zasadniczo, zgodnie z pytaniem ... czy kolejność funkcji LINQ ma znaczenie dla wydajności ? Oczywiście wyniki nadal musiałyby być identyczne ...

Przykład:

myCollection.OrderBy(item => item.CreatedDate).Where(item => item.Code > 3);
myCollection.Where(item => item.Code > 3).OrderBy(item => item.CreatedDate);

Oba zwracają te same wyniki, ale są w innej kolejności LINQ. Zdaję sobie sprawę, że zmiana kolejności niektórych pozycji przyniesie inne rezultaty i nie martwię się o to. Moim głównym zmartwieniem jest to, aby wiedzieć, czy przy uzyskiwaniu takich samych wyników zamawianie może wpłynąć na wydajność. I nie tylko w przypadku 2 wykonanych przeze mnie wywołań LINQ (OrderBy, Where), ale w przypadku wszystkich wywołań LINQ.

Michael
źródło
9
Świetne pytanie.
Robert S.
Jest jeszcze bardziej oczywiste, że optymalizacja dostawcy ma znaczenie w przypadku bardziej pedantycznego przypadku var query = myCollection.OrderBy(item => item.Code).Where(item => item.Code == 3);.
Mark Hurd,
1
Zasługujesz na głosowanie za :), ciekawe pytania. Rozważę to, pisząc mój Linq do Entities w EF.
GibboK,
1
@GibboK: Zachowaj ostrożność podczas próby „optymalizacji” zapytań LINQ (patrz odpowiedź poniżej). Czasami tak naprawdę niczego nie optymalizujesz. Podczas próby optymalizacji najlepiej jest użyć narzędzia profilującego.
myermian

Odpowiedzi:

147

Będzie to zależeć od używanego dostawcy LINQ. W przypadku LINQ to Objects może to mieć ogromne znaczenie. Załóżmy, że faktycznie mamy:

var query = myCollection.OrderBy(item => item.CreatedDate)
                        .Where(item => item.Code > 3);

var result = query.Last();

Wymaga to posortowania, a następnie przefiltrowania całej kolekcji . Gdybyśmy mieli milion pozycji, z których tylko jeden miałby kod większy niż 3, tracilibyśmy dużo czasu na zamawianie wyników, które zostałyby wyrzucone.

Porównaj to z odwrotną operacją, najpierw filtruj:

var query = myCollection.Where(item => item.Code > 3)
                        .OrderBy(item => item.CreatedDate);

var result = query.Last();

Tym razem zamawiamy tylko przefiltrowane wyniki, które w przykładowym przypadku „tylko jednego elementu pasującego do filtra” będą dużo bardziej wydajne - zarówno w czasie, jak i przestrzeni.

To również mogło mieć znaczenie w czy kwerenda wykonuje prawidłowo, czy nie. Rozważać:

var query = myCollection.Where(item => item.Code != 0)
                        .OrderBy(item => 10 / item.Code);

var result = query.Last();

W porządku - wiemy, że nigdy nie będziemy dzielić przez 0. Ale jeśli wykonamy porządkowanie przed filtrowaniem, zapytanie zgłosi wyjątek.

Jon Skeet
źródło
2
@Jon Skeet, czy istnieje dokumentacja dotycząca Big-O dla każdego dostawcy LINQ i funkcji? Czy jest to po prostu przypadek „każde wyrażenie jest unikalne dla danej sytuacji”.
michael
1
@michael: Nie jest to dobrze udokumentowane, ale jeśli czytasz moją serię blogów „Edulinq”, myślę, że opowiem o tym z rozsądnymi szczegółami.
Jon Skeet
3
@michael: możesz go znaleźć tutaj msmvps.com/blogs/jon_skeet/archive/tags/Edulinq/default.aspx
VoodooChild
3
@gdoron: Szczerze mówiąc, nie jest do końca jasne, co masz na myśli. Wygląda na to, że możesz chcieć napisać nowe pytanie. Pamiętaj, że Queryable w ogóle nie próbuje zinterpretować zapytania - jego zadaniem jest wyłącznie zachowanie zapytania, aby ktoś inny mógł je zinterpretować. Należy również zauważyć, że LINQ to Objects nawet nie używa drzew wyrażeń.
Jon Skeet,
1
@gdoron: Chodzi o to, że to zadanie dostawcy, a nie zadanie Queryable. I nie powinno to mieć znaczenia również w przypadku korzystania z Entity Framework. Ma to jednak znaczenie dla LINQ to Objects. Ale tak, zdecydowanie zadaj inne pytanie.
Jon Skeet
17

Tak.

Ale dokładnie, jaka jest ta różnica wydajności, zależy od tego, jak podstawowe drzewo wyrażeń jest oceniane przez dostawcę LINQ.

Na przykład zapytanie może być wykonywane szybciej za drugim razem (z klauzulą ​​WHERE jako pierwszą) dla LINQ-to-XML, ale szybciej za pierwszym razem dla LINQ-to-SQL.

Aby dowiedzieć się dokładnie, jaka jest różnica w wydajności, najprawdopodobniej będziesz chciał sprofilować swoją aplikację. Jak zawsze w przypadku takich rzeczy, jednak przedwczesna optymalizacja zwykle nie jest warta wysiłku - może się okazać, że ważniejsze są problemy inne niż wydajność LINQ.

Jeremy McGee
źródło
5

W twoim konkretnym przykładzie może to mieć wpływ na wydajność.

Pierwsze zapytanie: Twoje OrderBywywołanie musi wykonać iterację przez całą sekwencję źródłową, w tym elementy, w których Codejest 3 lub mniej. WherePunkt następnie musi również iteracyjne cały uporządkowane sekwencji.

Drugie zapytanie: WhereWywołanie ogranicza sekwencję tylko do tych elementów, w których Codejest większa niż 3. OrderByWywołanie musi wtedy tylko przejść przez zredukowaną sekwencję zwróconą przez Wherewywołanie.

LukeH
źródło
3

W Linq-To-Objects:

Sortowanie jest raczej powolne i zajmuje O(n)pamięć. Wherez drugiej strony jest stosunkowo szybki i wykorzystuje stałą pamięć. Zrobienie tego Wherenajpierw będzie szybsze, a dla dużych kolekcji znacznie szybsze.

Zmniejszone obciążenie pamięci może być również znaczące, ponieważ alokacje na dużej stercie obiektów (wraz z ich kolekcją) są z mojego doświadczenia stosunkowo drogie.

CodesInChaos
źródło
1

Oczywiście wyniki nadal musiałyby być identyczne ...

Pamiętaj, że tak nie jest - w szczególności poniższe dwa wiersze dadzą różne wyniki (dla większości dostawców / zbiorów danych):

myCollection.OrderBy(o => o).Distinct();
myCollection.Distinct().OrderBy(o => o);
BlueRaja - Danny Pflughoeft
źródło
1
Nie, chodziło mi o to, że wyniki powinny być identyczne, aby nawet rozważyć optymalizację. Nie ma sensu „optymalizować” czegoś i uzyskiwać inny wynik.
michael
1

Warto zauważyć, że rozważając sposób optymalizacji zapytania LINQ , należy zachować ostrożność . Na przykład, jeśli używasz deklaratywnej wersji LINQ, aby wykonać następujące czynności:

public class Record
{
    public string Name { get; set; }
    public double Score1 { get; set; }
    public double Score2 { get; set; }
}


var query = from record in Records
            order by ((record.Score1 + record.Score2) / 2) descending
            select new
                   {
                       Name = record.Name,
                       Average = ((record.Score1 + record.Score2) / 2)
                   };

Jeśli z jakiegoś powodu zdecydujesz się „zoptymalizować” zapytanie, zapisując najpierw średnią w zmiennej, nie uzyskasz pożądanych wyników:

// The following two queries actually takes up more space and are slower
var query = from record in Records
            let average = ((record.Score1 + record.Score2) / 2)
            order by average descending
            select new
                   {
                       Name = record.Name,
                       Average = average
                   };

var query = from record in Records
            let average = ((record.Score1 + record.Score2) / 2)
            select new
                   {
                       Name = record.Name,
                       Average = average
                   }
            order by average descending;

Wiem, że niewielu ludzi używa deklaratywnego LINQ dla obiektów, ale jest to dobre źródło do przemyśleń.

myermian
źródło
0

To zależy od trafności. Załóżmy, że jeśli masz bardzo mało pozycji z kodem = 3, następne zamówienie będzie działać na małym zestawie kolekcji, aby uzyskać zamówienie według daty.

Natomiast jeśli masz wiele elementów z tą samą datą CreatedDate, następne zamówienie będzie działać na większym zestawie kolekcji, aby uzyskać zamówienie według daty.

Tak więc w obu przypadkach będzie różnica w wydajności

Pankaj Upadhyay
źródło