遍历Linq结果时出现奇怪的缓慢问题

3

在探索最近的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

计算需要 0.13 秒,但显示需要超过 11 秒?这是什么原因?

2
请记住:查询表达式的结果是一个查询,而不是查询的结果。只有在请求查询结果时,您才能获得查询的结果。 - Eric Lippert
顺便提一下,我后面发布的版本应该会更快...但是延迟执行才是关键点。 - Marc Gravell
6个回答

5

linq查询之所以看起来执行速度快,是因为在定义时实际上没有计算任何内容,因为linq使用延迟执行,即直到您开始枚举结果才会执行“真正”的工作。


5

原因是查询直到枚举时才会实际运行。在LINQ to objects中,它只设置了一堆委托,在枚举器上迭代时调用。如果您要添加ToList()来使查询材料化,您将看到所需的时间将转移到设置而不是显示。


3

对于许多LINQ提供程序而言,从“alg start”到“alt end”的内容只是被解析 - 实际表达式直到您开始枚举结果才会被评估。因此,“qry”变量的实际创建速度很快(只需设置一个可枚举对象,该对象将实际执行查询中的逻辑),但枚举它的速度较慢。


3
LINQ代码仅将查询表达式转换为查询对象,这不需要太多时间。只有在foreach循环中查询才会实际执行。
顺便说一下,你不应该使用DateTime.Now来测量性能,而应该使用Stopwatch类,因为它更加精确。

3

查询实际上直到您迭代它才计算。在此之前,它就像一个SQL语句一样,等待执行。


2

这个问题正在进行暴力破解;在这种情况下,LINQ非常方便 - 我在这里讨论了这个问题:Brute force (but lazily)

仅仅是为了补充之前的一些答案:

LINQ通常是设计成延迟执行的,意味着在开始迭代结果之前不会发生任何事情。这通常是通过迭代器块来实现的;考虑以下代码之间的区别:

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

并且:

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

区别在于第二个版本在调用Where时完成所有工作,返回单个结果,而第二个版本(通过迭代器块的魔法)仅在枚举器调用MoveNext()时才执行工作。迭代器块在《C#深度剖析》免费样品第6章中有更详细的讨论。
通常,这样做的优点是使查询具有可组合性——特别是对于基于数据库的查询非常重要,但对于常规工作同样有效。
请注意,即使使用迭代器块,也有第二个考虑因素;缓冲。考虑Reverse()——无论如何,要反转序列,首先需要找到序列的结尾。现在考虑并非所有序列都有结尾!与此相比,WhereSkipTake等可以在不缓冲的情况下过滤行(只需删除项目)。
这个斐波那契问题中,使用非缓冲、延迟的方法是一个很好的例子。
    foreach (long i in Fibonacci().Take(10)) {
        Console.WriteLine(i);
    }

没有延迟执行,这将永远无法完成。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接