C# Linq性能:(ElementAt,Count)vs(foreach)

C# Linq性能:(ElementAt,Count)vs(foreach),c#,performance,linq,C#,Performance,Linq,我使用(ElementAt,Count)和(foreach)遍历IEnumerable作为Linq查询的结果。令我惊讶的是,性能差异是原来的25-30倍!为什么呢 IEnumerable<double> result = ... simple Linq query that joins two tables ... returns about 600 items double total = 0; // Method 1: iterate with Count

我使用(ElementAt,Count)和(foreach)遍历IEnumerable作为Linq查询的结果。令我惊讶的是,性能差异是原来的25-30倍!为什么呢

IEnumerable<double> result =
     ... simple Linq query that joins two tables
     ... returns about 600 items

double total = 0;

// Method 1: iterate with Count and ElementAt
for( int i = 0; i < result.Count(); i++ )
{
    total += result.ElementAt(i);
}

// Method 2: iterate with foreach
foreach( double value in result )
{
    total += value;
}
IEnumerable结果=
... 连接两个表的简单Linq查询
... 退货约600件
双倍合计=0;
//方法1:使用Count和ElementAt进行迭代
对于(int i=0;i
因为
ElementAt
每次调用时都在迭代
IEnumerable
IEnumerable
s没有索引,因此必须使用
GetEnumerator()实现
ElementAt

为什么不呢

total = result.Sum();

因为每次调用都会在列表上迭代。简单的解决方案是在迭代之前调用.ToList(),更好的解决方案是停止迭代

var theList=result.ToList();
for(int i=0;i
更好的解决方案:

total = result.Sum();

性能差异是由于
IEnumerable
不允许按索引获取,因此每次在第一个循环中调用
ElementAt
时,它必须遍历每个项,直到它到达您请求的元素。

方法是O(n),除非
IEnumerable
所代表的实际具体类对其进行了优化。这意味着每次调用它时,它都必须遍历整个可枚举项,以找到
n
处的元素。更不用说,因为在
for
循环的条件部分中有
i
,所以每次都必须循环整个枚举才能得到该计数


第二种方法是,只循环一次
result

如果我冒险猜测,result.Count()调用是非延迟的,实际上会命中数据库,而foreach不会。如果你颠倒顺序,你可能会得到相反的结果。也可以只做
total=result.Sum()

第一个可能相当于:

double total = 0;

int i = 0;
while(true)
{
  int max = /*Database call to obtain COUNT(*) of join*/
  if(max > i)
    break;
  int j = 0;
  foreach(double value in result)
  {
    if(j++ == i)
    {
      total += value;
      break;
    }
  }
  ++i
}
double total = 0;

int i = 0;
while(true)
{
  int max = 0;
  foreach(double value in result)
    ++max;
  if(max > i)
    break;
  int j = 0;
  foreach(double value in result)
  {
    if(j++ == i)
    {
      total += value;
      break;
    }
  }
  ++i
}
或者甚至可以等同于:

double total = 0;

int i = 0;
while(true)
{
  int max = /*Database call to obtain COUNT(*) of join*/
  if(max > i)
    break;
  int j = 0;
  foreach(double value in result)
  {
    if(j++ == i)
    {
      total += value;
      break;
    }
  }
  ++i
}
double total = 0;

int i = 0;
while(true)
{
  int max = 0;
  foreach(double value in result)
    ++max;
  if(max > i)
    break;
  int j = 0;
  foreach(double value in result)
  {
    if(j++ == i)
    {
      total += value;
      break;
    }
  }
  ++i
}
或者,它甚至可以在每次出现在上述代码中时重新查询以获得
结果


另一方面,
Count()
可以通过一次属性访问获得,
ElementAt()
可以是O(1),如果这些都是基于允许这种优化的结构,并且这种优化确实是可用的(例如,它是针对
List
)。

这是关于理解延迟执行的!当一个查询执行多次时,运行时间会急剧增加。LINQ可以更快,但您确实需要根据如何使用查询结果做出选择


看看这篇文章。它分析了这个问题。

差异在哪一个方向(什么更快)?
Count()
将迭代整个可枚举项。每个
ElementAt(n)
将迭代到第n个元素
foreach
将对整个可枚举项迭代一次。我不知道所有的优化,如果枚举也是一个
IList
ICollection
。你应该清楚地说明哪种方法更快。。。原样
Count()
。顺便说一句,不需要lambda表达式。只要
result.Sum()
就可以了。@Jon Skeet,怀疑是这样,但必须先去检查一下。现在似乎很明显,ala
Select()
total+=p用于演示目的。实际的逻辑是不同的。因为他们说它来自连接,所以我猜result.Count()实际上变成了
SQL Count(*)
。但几乎没有什么进步!