【问题标题】:Why is this LINQ so slow?为什么这个 LINQ 这么慢?
【发布时间】:2010-07-01 15:43:31
【问题描述】:

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

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 的:

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,那么你在某个地方犯了一个错误。

【问题讨论】:

  • 可能是因为它必须做更多数量级的工作。你为什么认为它不应该再花更多时间?
  • @Jay:他一定在使用 LINQPad。这是一种将结果转储到结果窗格的内置方法。
  • 嗨,杰。 Dump 只是一个将其发送到控制台的 LinqPad 扩展方法。
  • 您可以将 Dump() 替换为 ToList() 并观察减速,无需 LINQPad。
  • 我应该为自己嘲笑 Take(1).Dump() 而感到羞耻吗?

标签: linq


【解决方案1】:

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

这里的前两个 LINQ 语句本质上是二次的。由于您的“匹配”元素可能需要遍历整个“数据”序列才能找到匹配项,随着您在范围内的进展,该元素的时间长度将逐渐变长。例如,第 10000 个元素将强制引擎遍历原始序列的所有 10000 个元素以找到匹配项,这使得这是一个 O(N^2) 操作。

“test3”操作将其带到了一个全新的痛苦水平,因为它在第二个单曲中“平方”了 O(N^2) 操作 - 迫使它在第一个单曲之上执行另一个二次运算 -这将是大量的操作。

每次对匹配进行 data.Single(...) 时,您都在进行 O(N^2) 操作 - 第三次测试基本上变成 O(N^4),这将是数量级慢一点。

【讨论】:

  • 如何迭代整个序列二次?为什么第三个测试会平方它,从内到外,它应该与执行 test1 并将结果插入 test2 相同?
  • @stovroz:当您调用 data.Single 时,您(可能)正在为每个数据元素进行“数据”的完整枚举。这意味着每次从 Select() 返回一个新项目时,您可能必须遍历所有数据才能找到“匹配项”。在第三种情况下,您正在(已经)二次元素内进行嵌套,它基本上在 N^2 操作中执行 N^2 操作...
  • 嗨里德科普西。我假设 data.Single 肯定在做一个完整的枚举数据,因为它需要确保只有一个。无论如何,所以 test3 的 data.Single(y => y.Value == x.Value).Index 为我们提供了 O(N) 中 Index 的值,从而减少了对 test2 的 data.Single(z => z.Index ==索引),这也是 O(N)。而且我只要求 N 中的 Take(1),那么整个事情不是应该的 O(N),这是怎么回事?
  • @stovroz:拿 test1:当你迭代时,它是 O(N)。但是,每个元素都必须执行额外的 data.Single [O(N)] 调用,这意味着您是 O(N^2)。当您执行 test3 时,您正在内部嵌套中执行另一个 O(N),这使得它再次“平方”,这导致 10^16 总迭代 - 因此,它非常慢。
  • 每个元素必须做一个额外的数据是什么意思。单次调用?我只拿第一个,估计是懒惰的评估。
【解决方案2】:

固定。

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 必须完全枚举其来源以检查多个实例。

【讨论】:

  • 如果原始代码在几秒钟内生成 10^16 个实例,我将把我的电脑卖给 NASA。
  • 好吧,转储前的 take(1) 将它削减到 10^12。
猜你喜欢
  • 2013-02-28
  • 1970-01-01
  • 1970-01-01
  • 2023-04-02
  • 2011-03-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-05
相关资源
最近更新 更多