2012-06-14 14 views
6

Znam różnicę między IQueryable a IEnumerable i wiem, że kolekcje są obsługiwane przez Linq To Objects za pośrednictwem interfejsu IEnumerable.Dlaczego IQueryable jest dwa razy szybszy niż IEnumerable przy korzystaniu z Linq To Objects

Co jest dla mnie zagadką, że zapytania są wykonywane dwukrotnie szybciej, gdy kolekcja jest konwertowana na IQueryable.

Niech l być wypełniony obiektu typu listy, zapytanie LINQ dwukrotnie raz za szybko, jeśli lista l przekształca się w IQueryable poprzez l.AsQueryable().

Napisałem prosty test z VS2010SP1 i .NET 4.0, który pokazuje to:

private void Test() 
{ 
    const int numTests = 1; 
    const int size = 1000 * 1000; 
    var l = new List<int>(); 
    var resTimesEnumerable = new List<long>(); 
    var resTimesQueryable = new List<long>(); 
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); 

    for (int x=0; x<size; x++) 
    { 
    l.Add(x); 
    } 

    Console.WriteLine("Testdata size: {0} numbers", size); 
    Console.WriteLine("Testdata iterations: {0}", numTests); 

    for (int n = 0; n < numTests; n++) 
    { 
    sw.Restart(); 
    var result = from i in l.AsEnumerable() where (i % 10) == 0 && (i % 3) != 0 select i; 
    result.ToList(); 
    sw.Stop(); 
    resTimesEnumerable.Add(sw.ElapsedMilliseconds); 
    } 
    Console.WriteLine("TestEnumerable"); 
    Console.WriteLine(" Min: {0}", Enumerable.Min(resTimesEnumerable)); 
    Console.WriteLine(" Max: {0}", Enumerable.Max(resTimesEnumerable)); 
    Console.WriteLine(" Avg: {0}", Enumerable.Average(resTimesEnumerable)); 

    for (int n = 0; n < numTests; n++) 
    { 
    sw.Restart(); 
    var result = from i in l.AsQueryable() where (i % 10) == 0 && (i % 3) != 0 select i; 
    result.ToList(); 
    sw.Stop(); 
    resTimesQueryable.Add(sw.ElapsedMilliseconds); 
    } 
    Console.WriteLine("TestQuerable"); 
    Console.WriteLine(" Min: {0}", Enumerable.Min(resTimesQueryable)); 
    Console.WriteLine(" Max: {0}", Enumerable.Max(resTimesQueryable)); 
    Console.WriteLine(" Avg: {0}", Enumerable.Average(resTimesQueryable)); 
} 

Uruchomienie tego testu (z woli numTests == 1 i 10) daje następujący wynik:

Testdata size: 1000000 numbers 
Testdata iterations: 1 
TestEnumerable 
    Min: 44 
    Max: 44 
    Avg: 44 
TestQuerable 
    Min: 37 
    Max: 37 
    Avg: 37 

Testdata size: 1000000 numbers 
Testdata iterations: 10 
TestEnumerable 
    Min: 22 
    Max: 29 
    Avg: 23,9 
TestQuerable 
    Min: 12 
    Max: 22 
    Avg: 13,9 

Powtórzenie testu, ale zmiana kolejności (tj. Pierwszy pomiar IQuerable, a następnie IEnumerable) daje wyniki różnicowania!

Testdata size: 1000000 numbers 
Testdata iterations: 1 
TestQuerable 
    Min: 75 
    Max: 75 
    Avg: 75 
TestEnumerable 
    Min: 25 
    Max: 25 
    Avg: 25 

Testdata size: 1000000 numbers 
Testdata iterations: 10 
TestQuerable 
    Min: 12 
    Max: 28 
    Avg: 14 
TestEnumerable 
    Min: 22 
    Max: 26 
    Avg: 23,4 

Oto moje pytania:

  1. Co robię źle?
  2. Dlaczego jest IEnumerable szybsza, jeśli test zostanie wykonany po teście IQueryable?
  3. Dlaczego numer IQueryable jest szybszy, gdy numer nie. serii testów jest zwiększona?
  4. Czy jest nałożona kara za pomocą IQueryable zamiast IEnumerable?

Zadaję te pytania, ponieważ zastanawiałem się, którego użyć do mojego interfejsu repozytorium. Teraz wysyłają zapytania do kolekcji w pamięci (Linq to Objects), ale w przyszłości ten może być źródłem danych SQL. Jeśli zaprojektuję klasy repozytoriów teraz z IQueryable, mogę bezboleśnie przejść później na Linq na SQL. Jednakże, jeśli występuje kara za wykonanie, a następnie przyklejenie się do IEnumerable, podczas gdy nie ma w tym przypadku SQL, wydaje się mądrzejszy.

+0

Nie określasz, czy budujesz w trybie zwolnienia lub debugowania, i nie "wczytujesz" najpierw funkcji, aby uzyskać szum. (Myślę). Na dłuższą metę różnica kilku ms powyżej 10.000.000 iteracji nie wydaje się zbyt wielka. – asawyer

+0

Budowałem w trybie debugowania. Przejście do trybu zwolnienia i dodanie uruchomienia "inicjalizacji" (tj. Wykonanie i zmaterializowanie każdego zapytania raz) pomogło: Teraz kod ** IEnumerable ** działa nieco szybciej niż ** kod IQueryable ** (11ms vs 12ms). I właśnie tego się spodziewałem. Więc mój testowy kod był wadliwy. Dzięki za wskazówki! – rbu

+0

"Mogę bezboleśnie przełączyć się później na Linq na SQL" ... Bardzo chciałbym usłyszeć, jak to działa. –

Odpowiedz

5

Korzystanie LINQPad zbadać kod IL, oto co widzę:

Na ten kod:

var l = Enumerable.Range(0,100); 

var result = from i in l.AsEnumerable() where (i % 10) == 0 && (i % 3) != 0 select i; 

ten jest generowany:

IL_0001: ldc.i4.0  
IL_0002: ldc.i4.s 64 
IL_0004: call  System.Linq.Enumerable.Range 
IL_0009: stloc.0  
IL_000A: ldloc.0  
IL_000B: call  System.Linq.Enumerable.AsEnumerable 
IL_0010: ldsfld  UserQuery.CS$<>9__CachedAnonymousMethodDelegate1 
IL_0015: brtrue.s IL_002A 
IL_0017: ldnull  
IL_0018: ldftn  b__0 
IL_001E: newobj  System.Func<System.Int32,System.Boolean>..ctor 
IL_0023: stsfld  UserQuery.CS$<>9__CachedAnonymousMethodDelegate1 
IL_0028: br.s  IL_002A 
IL_002A: ldsfld  UserQuery.CS$<>9__CachedAnonymousMethodDelegate1 
IL_002F: call  System.Linq.Enumerable.Where 
IL_0034: stloc.1  

b__0: 
IL_0000: ldarg.0  
IL_0001: ldc.i4.s 0A 
IL_0003: rem   
IL_0004: brtrue.s IL_0011 
IL_0006: ldarg.0  
IL_0007: ldc.i4.3  
IL_0008: rem   
IL_0009: ldc.i4.0  
IL_000A: ceq   
IL_000C: ldc.i4.0  
IL_000D: ceq   
IL_000F: br.s  IL_0012 
IL_0011: ldc.i4.0  
IL_0012: stloc.0  
IL_0013: br.s  IL_0015 
IL_0015: ldloc.0  
IL_0016: ret   

I dla tego kodu:

var l = Enumerable.Range(0,100); 

var result = from i in l.AsQueryable() where (i % 10) == 0 && (i % 3) != 0 select i; 

We uzyskać to:

IL_0001: ldc.i4.0  
IL_0002: ldc.i4.s 64 
IL_0004: call  System.Linq.Enumerable.Range 
IL_0009: stloc.0  
IL_000A: ldloc.0  
IL_000B: call  System.Linq.Queryable.AsQueryable 
IL_0010: ldtoken  System.Int32 
IL_0015: call  System.Type.GetTypeFromHandle 
IL_001A: ldstr  "i" 
IL_001F: call  System.Linq.Expressions.Expression.Parameter 
IL_0024: stloc.2  
IL_0025: ldloc.2  
IL_0026: ldc.i4.s 0A 
IL_0028: box   System.Int32 
IL_002D: ldtoken  System.Int32 
IL_0032: call  System.Type.GetTypeFromHandle 
IL_0037: call  System.Linq.Expressions.Expression.Constant 
IL_003C: call  System.Linq.Expressions.Expression.Modulo 
IL_0041: ldc.i4.0  
IL_0042: box   System.Int32 
IL_0047: ldtoken  System.Int32 
IL_004C: call  System.Type.GetTypeFromHandle 
IL_0051: call  System.Linq.Expressions.Expression.Constant 
IL_0056: call  System.Linq.Expressions.Expression.Equal 
IL_005B: ldloc.2  
IL_005C: ldc.i4.3  
IL_005D: box   System.Int32 
IL_0062: ldtoken  System.Int32 
IL_0067: call  System.Type.GetTypeFromHandle 
IL_006C: call  System.Linq.Expressions.Expression.Constant 
IL_0071: call  System.Linq.Expressions.Expression.Modulo 
IL_0076: ldc.i4.0  
IL_0077: box   System.Int32 
IL_007C: ldtoken  System.Int32 
IL_0081: call  System.Type.GetTypeFromHandle 
IL_0086: call  System.Linq.Expressions.Expression.Constant 
IL_008B: call  System.Linq.Expressions.Expression.NotEqual 
IL_0090: call  System.Linq.Expressions.Expression.AndAlso 
IL_0095: ldc.i4.1  
IL_0096: newarr  System.Linq.Expressions.ParameterExpression 
IL_009B: stloc.3  
IL_009C: ldloc.3  
IL_009D: ldc.i4.0  
IL_009E: ldloc.2  
IL_009F: stelem.ref 
IL_00A0: ldloc.3  
IL_00A1: call  System.Linq.Expressions.Expression.Lambda 
IL_00A6: call  System.Linq.Queryable.Where 
IL_00AB: stloc.1  

Więc wydaje się, że różnica jest wersja AsQuerable buduje drzewa wyrażenie, AsEnumerable nie.

+1

Tak, różnica między IEnumerable i IQueriable jest taka, że ​​pierwsza używa delegatów, podczas gdy druga używa drzewek wyrażeń. Ale to nie odpowiada na żadne z moich pytań :-( – rbu