任何人都可以解释一下,为什么下面的第三个查询比其他查询要慢几个数量级,因为它不应该比按顺序执行前两个更长时间?
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为特征,你就会在某个地方出错。
答案 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();
在原始代码中,
Single和First不同。如果遇到多个实例,则单次抛出。 Single必须完全枚举其源以检查多个实例。