为什么这个LINQ这么慢?

时间:2010-07-01 15:43:32

标签: linq

任何人都可以解释一下,为什么下面的第三个查询比其他查询要慢几个数量级,因为它不应该比按顺序执行前两个更长时间?

var data = Enumerable.Range(0, 10000).Select(x => new { Index = x, Value = x + " is the magic number"}).ToList();
var test1 = data.Select(x => new { Original = x, Match = data.Single(y => y.Value == x.Value) }).Take(1).Dump();
var test2 = data.Select(x => new { Original = x, Match = data.Single(z => z.Index == x.Index) }).Take(1).Dump();
var test3 = data.Select(x => new { Original = x, Match = data.Single(z => z.Index == data.Single(y => y.Value == x.Value).Index) }).Take(1).Dump();
编辑:我已经在原始数据生成中添加了.ToList(),因为我不希望任何重复生成的数据使问题蒙上阴影。

我只是想了解为什么这个代码如此缓慢,而不是寻找更快的替代方案,除非它对此事有所了解。我想如果Linq被懒惰地评估,我只是在找第一项(Take(1))然后test3's:

data.Select(x => new { Original = x, Match = data.Single(z => z.Index == data.Single(y => y.Value == x.Value).Index) }).Take(1);

可以减少到:

data.Select(x => new { Original = x, Match = data.Single(z => z.Index == 1) }).Take(1)

在O(N)中,数据的第一项在内部Single()对数据进行一次完整扫描后成功匹配,剩余的Single()再次扫描数据。所以O(N)仍然如此。

它显然是以更长时间的方式处理,但我并不真正理解如何或为何。

Test3需要花费几秒的时间来运行,所以我认为我们可以放心地假设,如果你的答案以数字10 ^ 16为特征,你就会在某个地方出错。

2 个答案:

答案 0 :(得分:7)

前两个“测试”是相同的,都很慢。第三个增加了另一个整体缓慢程度。

这里的前两个LINQ语句本质上是二次语句。由于“匹配”元素可能需要遍历整个“数据”序列以便找到匹配项,因此当您在整个范围内前进时,该元素的时间长度将逐渐变长。例如,第10000个元素将强制引擎迭代原始序列的所有10000个元素以找到匹配,从而使其成为O(N ^ 2)运算。

“test3”操作将这一点带到了一个全新的痛苦程度,因为它正在“平衡”第二个单独的O(N ^ 2)操作 - 迫使它在第一个单独的操作上进行另一个二次操作 - 这将是一项大量的行动。

每次你使用匹配进行data.Single(...),你正在进行O(N ^ 2)操作 - 第三次测试基本上变成O(N ^ 4),这将是数量级慢。

答案 1 :(得分:1)

固定。

var data = Enumerable.Range(0, 10000)
  .Select(x => new { Index = x, Value = x + " is the magic number"})
  .ToList();

var forward = data.ToLookup(x => x.Index); 
var backward = data.ToLookup(x => x.Value);

var test1 = data.Select(x => new { Original = x,
  Match = backward[x.Value].Single()
} ).Take(1).Dump();
var test2 = data.Select(x => new { Original = x,
  Match = forward[x.Index].Single()
} ).Take(1).Dump();
var test3 = data.Select(x => new { Original = x,
  Match = forward[backward[x.Value].Single().Index].Single()
} ).Take(1).Dump(); 

在原始代码中,

  • data.ToList()生成10,000个实例(10 ^ 4)。
  • data.Select(data.Single())。ToList()生成100,000,000个实例(10 ^ 8)。
  • data.Select(data.Single(data.Single()))。ToList()生成100,000,000,000,000,000个实例(10 ^ 16)。

Single和First不同。如果遇到多个实例,则单次抛出。 Single必须完全枚举其源以检查多个实例。