2013-04-18 14 views
6

Przeglądanie sekcji aplikacji webowej Pracuję dzisiaj z profilerem wydajności. Myślałem, że Unia powoduje pewne opóźnienia, ale zamiast tego znalazła inne zaskakujące wyniki.Wydajność FirstOrDefault()

Jedną z przyczyn spowolnienia okazała się FirstOrDefault.

Było bardzo prostej kwerendy LINQ, który wyglądał tak:

foreach(Report r in reports) 
    IDTOStudy study = studies.FirstOrDefault(s => s.StudyID == r.StudyID); 

stworzyłem małą sposób powielać zachowanie Pomyślałem FirstOrDefault robi.

private IDTOStudy GetMatchingStudy(Report report, IList<IDTOStudy> studies) 
{ 
    foreach (var study in studies) 
    if (study.StudyID == report.StudyID) 
     return study; 

    return null; 
} 

Metoda ta otrzymuje FirstOrDefault wyglądać tak:

foreach(Report r in reports) 
    IDTOStudy study = GetMatchingStudy(r, studies); 

Patrząc na nowy kod uruchomiony z profilera wydajności pokazał FirstOrDefault wziąć dwa razy tak długo, jak zakończyć moją nową metodą. To był szok, aby zobaczyć.

Muszę zrobić coś nieprawidłowego z zapytaniem FirstOrDefault(). Co to jest?

Czy FirstOrDefault() ukończyć całe zapytanie, a następnie podjąć pierwszy element?

Jak mogę przyspieszyć i użyć FirstOrDefault()?

Edit 1:

Jeden dodatkowy punkt zauważyłem jest to, że profiler mówi mi maxing mojego procesora na obu tych wdrożeń. To także coś, czego nie obchodzi mnie i nie oczekiwałem. Dodatkowa metoda, którą dodałem, nie zmniejszyła tego skoku, tylko skróciła jego czas trwania o połowę.

Edit 3:

Wykorzystanie badań w słowniku poprawiła czas pracy ogromnie. Zdecydowanie będzie wyglądać kod zatwierdzony. Nie odpowiada jednak na pytanie FirstOrDefault.

Edit 2:

Oto wnioskowana w prosty app konsoli przykładowy kod. Mój bieg nadal pokazuje, że w większości przypadków FirstOrDefault trwa dłużej.

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Reactive.Linq; 
using System.Reactive.Concurrency; 
using System.Diagnostics; 

namespace TestCode 
{ 
    public class Program 
    { 
     public List<IntHolder> list; 

     public static void Main(string[] args) 
     { 
      var prog = new Program(); 
      prog.list = new List<IntHolder>(); 

      prog.Add50000Items(); 
      prog.list.Add(new IntHolder() { Num = 12345 }); 
      prog.Add50000Items(); 

      var stopwatch = new Stopwatch(); 
      stopwatch.Start(); 
      prog.list.FirstOrDefault(n => n.Num == 12345); 
      stopwatch.Stop(); 

      Console.WriteLine("First run took: " + stopwatch.ElapsedTicks); 
      var lookingFor = new IntHolder() { Num = 12345 }; 

      stopwatch.Reset(); 
      stopwatch.Start(); 
      prog.GetMatching(lookingFor); 
      stopwatch.Stop(); 
      Console.WriteLine("Second run took: " + stopwatch.ElapsedTicks); 
      Console.ReadLine(); 
     } 

     public void Add50000Items() 
     { 
      var rand = new Random(); 

      for (int i = 0; i < 50000; i++) 
       list.Add(new IntHolder() { Num = rand.Next(100000) }); 
     } 

     public IntHolder GetMatching(IntHolder num) 
     { 
      foreach (var number in list) 
       if (number.Num == num.Num) 
        return number; 

      return null; 
     } 
    } 

    public class IntHolder 
    { 
     public int Num { get; set; } 
    } 
} 
+0

Jeśli używasz LINQ do SQL lub EF, następnie 'FirstOrDefault()' 'powinien po prostu wygenerować TOP 1 ' Zapytanie –

+0

jakie są badania, czy jest to obiekt orm (np. dbset z Entity Framework lub podobnego)? –

+0

@lazyberezovsky podejrzewam, że pierwszy przykład generuje O (n) i drugi O (1) zapytań db –

Odpowiedz

3

co myślę dzieje (choć byłoby dobrze, aby uzyskać trochę więcej informacji na temat konkretnego scenariusza, moje założenie, że jest to scenariusz DB na podstawie swoich klas dto) jest następujący:

foreach(Report r in reports) 
    IDTOStudy study = studies.FirstOrDefault(s => s.StudyID == r.StudyID); //Database query happens here for each report 


//The whole studies table is loaded into memory which means you only do one DB query and the actual firstordefault stuff is done in memory which is quicker than going over the network 
private IDTOStudy GetMatchingStudy(Report report, IList<IDTOStudy> studies) 
{ 
    foreach (var study in studies) 
    if (study.StudyID == report.StudyID) 
     return study; 

    return null; 
} 

Co to oznacza, że ​​w drugim przykładzie zoptymalizowano pod kątem obiegu bazy danych (co jest dobrym pomysłem).

Można udowodnić tę teorię poprzez sprawdzanie zapytań do bazy danych dzieje się za kulisami z czymś SQL Profiler

+3

Jedna uwaga dotycząca takiej optymalizacji - jeśli masz 1 milion badań w bazie danych, to załadowanie wszystkich do pamięci nie jest dobrą optymalizacją. –

+0

Lista badań DTO została wypełniona zapytaniem SQL, a następnie została już wymuszona na List with ToList(). Więc nie sądzę, żebym walił w DB - co też było moją pierwszą myślą. Aby wyjaśnić, na liście znajduje się około 20 000 badań. – Chris

+3

@lazyberezovsky yeah absolutnie, jeszcze lepszym rozwiązaniem tego problemu (który byłby ładnie skalowany) byłoby zrobienie tego wszystkiego po stronie bazy danych, a nie strony aplikacji –