2013-04-18 16 views
6

Guardando una sezione della webapp su cui lavoro oggi con un profiler delle prestazioni. Pensavo che un'Unione causasse alcuni ritardi, ma invece ho trovato altri risultati sorprendenti.Prestazioni di FirstOrDefault()

Una delle cause del rallentamento sembrava essere FirstOrDefault.

E 'stata una semplice query LINQ che si presentava così:

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

ho creato un piccolo metodo per riprodurre il comportamento ho pensato FirstOrDefault stava facendo.

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

    return null; 
} 

Questo metodo ha sostituito il FirstOrDefault a guardare come questo:

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

Guardando il nuovo codice in esecuzione con il profiler prestazioni ha mostrato FirstOrDefault di prendere il doppio del tempo per completare il mio nuovo metodo. Questo è stato uno shock da vedere.

Devo fare qualcosa di sbagliato con la query FirstOrDefault(). Che cos'è?

FirstOrDefault() completa l'intera query e quindi prende il primo elemento?

Come è possibile velocizzare e utilizzare FirstOrDefault()?

Edit 1:

Un ulteriore punto ho notato è che il profiler dice che sto maxing la mia CPU su entrambi questi implementazioni. Anche questo è qualcosa che non mi interessa e che non mi aspettavo. Il metodo aggiuntivo che ho aggiunto non ha ridotto il picco, ma ne ha ridotto la durata a metà.

Edit 3:

Mettere gli studi in un dizionario ha migliorato il tempo di esecuzione tremendamente. Sarà sicuramente come appare il codice impegnato. Tuttavia, non risponde alla domanda su FirstOrDefault.

Edit 2:

Ecco il codice di esempio richiesto in una semplice console app. La mia esecuzione mostra ancora che nella maggior parte dei casi FirstOrDefault richiede più tempo.

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

Se si utilizza LINQ to SQL o EF, allora 'FirstOrDefault()' dovrebbe semplicemente generare 'TOP 1' query –

+0

che cosa sono gli studi, si tratta di un oggetto orm (ad esempio un dbset da Entity Framework o simile)? –

+0

@lazyberezovsky il mio sospetto è che il primo esempio generi O (n) e il secondo O (1) db query –

risposta

3

quello che penso sta accadendo (anche se sarebbe bene per ottenere un po 'di informazioni extra sul tuo scenario specifico, la mia ipotesi è che questo è uno scenario DB, sulla base di classi DTO) è il seguente:

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; 
} 

Ciò significa che nel secondo esempio sono stati ottimizzati i roundtrip del database (che è una buona idea).

si può dimostrare questa teoria controllando le query di database che accadono dietro le quinte con qualcosa come SQL Profiler

+3

Una nota relativa a tale ottimizzazione: se si dispone di 1 milione di studi nel database, caricare tutto in memoria non è una buona ottimizzazione –

+0

L'elenco di studi DTO è stato popolato con una query SQL e quindi già stato forzato a Elenco con ToList(). Quindi non penso di battere il DB - che era anche il mio pensiero iniziale. E per chiarire, ci sono circa 20.000 studi nella lista. – Chris

+3

@lazyberezovsky si, assolutamente, una soluzione ancora migliore per questo problema (che sarebbe gradatamente scalabile) sarebbe quella di fare tutto questo lato del database non lato applicazione –

Problemi correlati