迭代Linq结果时出现奇怪的缓慢

在探索最近的Linq问题时,我注意到算法似乎相当慢。 深入挖掘我注意到它不是linq代码,但结果的输出后来耗费了很长时间。 (感谢Marc Gravel顺便说一下,我已经看过了最简单的Linq。)

码:

DateTime dt = DateTime.Now; Console.WriteLine("Alg Start " + dt.Second + "." + dt.Millisecond); var qry = from l in Enumerable.Range(100000, 999999) let s = l.ToString() let sReversed = new string(s.Reverse().ToArray()) from i in Enumerable.Range(3, 9) let t = (l * i).ToString() where t == sReversed select new { l, i }; dt = DateTime.Now; Console.WriteLine("Alg End " + dt.Second + "." + dt.Millisecond); foreach (var row in qry) Console.WriteLine("{0} x {1} = {2}", row.l, row.i, row.l * row.i); dt = DateTime.Now; Console.WriteLine("Disp End " + dt.Second + "." + dt.Millisecond); 

输出:

 Alg Start 20.257 Alg End 20.270 109989 x 9 = 989901 219978 x 4 = 879912 1099989 x 9 = 9899901 Disp End 31.322 

.13秒计算和超过11显示?!? 这是什么原因?

原因是在枚举之前查询实际上并不运行。 在LINQ to对象中,它只是设置了一堆代理,当你遍历枚举器时会被调用。 如果您要在查询中添加ToList()以实现它,您会看到所花费的时间将转移到设置并远离显示。

linq查询似乎快速执行的原因是因为linq使用延迟执行,所以在定义点上实际上没有计算任何内容,即在您开始枚举结果之前不会执行“实际”工作。

对于许多linq提供程序,只需解析“alg start”到“alt end” – 在实际开始枚举结果之前,不会评估实际表达式。 因此,“qry”变量的实际创建速度很快(只需设置一个实际执行查询中逻辑的枚举),但枚举它的速度较慢。

LINQ代码仅从查询表达式中创建查询对象,这不会花费很多时间。 只有在foreach中才是实际执行的查询。

顺便说一下,你不应该使用DateTime.Now来提高性能,而应该使用Stopwatch类,因为它更准确。

在迭代之前,查询实际上不会计算。 在那之前它就像一个SQL语句,等待执行。

这个问题正在做蛮力; 在这种情况下LINQ实际上非常方便 – 我在这里讨论过: 蛮力(但懒惰)

只是扩展一些以前的答案:

LINQ通常围绕延迟执行进行设计,这意味着在开始迭代结果之前不会发生任何事情。 这通常通过迭代器块完成; 考虑这些之间的区别:

 static IEnumerable Where(this IEnumerable data, Func predicate) { foreach(T item in data) { if(predicate(item)) yield return item; } } 

和:

 static IEnumerable Where(this IEnumerable data, Func predicate) { var list = new List(); foreach(T item in data) { if(predicate(item)) list.Add(item); } return list; } 

不同之处在于, 第二个版本在调用Where时返回所有工作,返回单个结果,其中 – 第二个版本(通过迭代器块的魔法)仅在枚举器调用MoveNext()时才起作用。 在深度C#的免费样本第6章中更详细地讨论了迭代器块。

通常,这样做的好处是它使查询可组合 – 对于基于数据库的查询尤其重要,但对于常规工作同样有效。

请注意,即使使用迭代器块,还有第二个考虑因素; 缓冲。 考虑Reverse() – 无论你怎么做,要反转一个序列,首先你需要找到序列的结尾。 现在考虑并非所有序列都结束! 将其与WhereSkipTake等对比 – 可以在缓冲的情况下过滤行(只需删除项目)。

在无限序列中使用它的一个很好的例子是这个Fibonacci问题 ,我们可以使用非缓冲的延迟方法:

  foreach (long i in Fibonacci().Take(10)) { Console.WriteLine(i); } 

没有延迟执行,这将永远不会完成。