为什么在使用Linq To Objects时,IQueryable比IEnumerable快两倍?

7
我知道IQueryable和IEnumerable之间的区别,也知道通过IEnumerable接口支持Linq To Objects的集合。
令我困惑的是,当集合转换为IQueryable时,查询的执行速度会快两倍。
设l是一个填充好的List对象,则如果列表l通过l.AsQueryable()转换为IQueryable,则linq查询的速度会变快一倍。
我在VS2010SP1和.NET 4.0上编写了一个简单的测试来证明这一点。
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 ) );
}

运行此测试(其中将numTests == 1和10)会产生以下输出:

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

重复测试,但更改顺序(即首先测量IQueryable,然后是IEnumerable)会产生不同的结果!

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

以下是我的问题:

  1. 我做错了什么?
  2. 为什么在执行 IQueryable 测试之后,使用 IEnumerable 更快?
  3. 为什么增加测试运行次数后,使用 IQueryable 更快?
  4. 使用 IQueryable 是否会产生影响?

我提出这些问题是因为我想知道在存储库接口中应该使用哪一个。目前它们在内存中查询集合(Linq to Objects),但未来可能会是 SQL 数据源。如果我现在使用 IQueryable 设计存储库类,以后可以轻松切换到 Linq to SQL。但是,如果存在性能惩罚,那么在没有 SQL 的情况下坚持使用 IEnumerable 似乎更明智。


你没有说明你是在发布模式还是调试模式下构建,而且你也没有“预热”函数,所以可能会看到抖动噪声。(我想)。从长远来看,在1000万次迭代中几毫秒的差异似乎并不是什么大问题。 - asawyer
我一直在调试模式下构建。切换到发布模式并添加一个“初始化”运行(即执行和实现每个查询一次)确实有所帮助:现在IEnumerable代码的执行速度略快于IQueryable代码(11ms vs 12ms)。这正是我预期的。因此,我的测试代码有误。感谢您提供的提示! - rbu
我可以毫不费力地随后转换到 Linq to SQL,我很想听听你的经验如何。 - Ronnie Overby
1个回答

6
使用linqpad来查看IL代码,以下是我看到的内容:
对于这段代码:
var l = Enumerable.Range(0,100);

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

这是生成的:

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         

对于这段代码:

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

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

我们得到了这个:
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     

看起来,两者之间的区别在于AsQuerable版本正在构建表达式树,而AsEnumerable则不是。

1
是的,IEnumerable和IQueriable的区别在于前者使用委托,而后者使用表达式树。但这并没有回答我的任何问题 :-( - rbu
它确实会 - 代码编译成树形数据结构后执行速度更快 - 但这并不意味着您的程序会更快 - 这取决于编译表达式树所需的时间。 - Sebastian Xawery Wiśniowiecki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接