14

我遇到了 LINQ 查询的性能问题,因此我创建了一个小型简化示例来演示以下问题。该代码采用小整数的随机列表并返回该列表,该列表被划分为几个较小的列表,每个列表的总数为 10 或更少。

问题是(正如我写的那样)代码需要 N 成倍增长。这只是一个 O(N) 问题。当 N=2500 时,代码在我的电脑上运行需要 10 多秒。

如果有人能解释发生了什么,我会非常感激。谢谢,马克。

int N = 250;
Random r = new Random();
var work = Enumerable.Range(1,N).Select(x => r.Next(0, 6)).ToList();
var chunks = new List<List<int>>();

// work.Dump("All the work.");  // LINQPad Print
var workEnumerable = work.AsEnumerable();

Stopwatch sw = Stopwatch.StartNew();
while(workEnumerable.Any())  // or .FirstorDefault() != null
{
    int soFar = 0;
    var chunk = workEnumerable.TakeWhile( x => 
                          {
                              soFar += x;               
                              return  (soFar <= 10);
                          }).ToList();
    chunks.Add(chunk);          // Commented out makes no difference.
    workEnumerable = workEnumerable.Skip(chunk.Count); // <== SUSPECT
}
sw.Stop();

// chunks.Dump("Work Chunks.");   // LINQPad Print
sw.Elapsed.Dump("Time elapsed.");
4

3 回答 3

11

.Skip()所做的是创建一个新的循环源,并且仅在第一个元素IEnumerable之后才开始产生结果。N你连锁谁知道有多少这些一个接一个。每次调用.Any()时,都需要再次遍历所有先前跳过的元素。

一般来说,在 LINQ 中设置非常复杂的运算符链并反复枚举它们是一个坏主意。此外,由于 LINQ 是一个查询 API,Skip()因此当您尝试实现的目标相当于修改数据结构时,类似的方法是一个糟糕的选择。

于 2012-11-20T23:12:34.277 回答
5

您有效地将 Skip() 链接到同一个可枚举对象上。在 250 个列表中,最后一个块将由一个惰性枚举创建,前面有 ~25 个“跳过”枚举器类。

如果你这样做了,你会发现事情变得更快了

workEnumerable = workEnumerable.Skip(chunk.Count).ToList();

但是,我认为整个方法可以改变。

如何使用标准 LINQ 来实现相同的目标:

在http://ideone.com/JIzpml上现场观看

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    private readonly static Random r = new Random();

    public static void Main(string[] args)
    {
        int N = 250;
        var work = Enumerable.Range(1,N).Select(x => r.Next(0, 6)).ToList();

        var chunks = work.Select((o,i) => new { Index=i, Obj=o })
            .GroupBy(e => e.Index / 10)
            .Select(group => group.Select(e => e.Obj).ToList())
            .ToList();

        foreach(var chunk in chunks)
            Console.WriteLine("Chunk: {0}", string.Join(", ", chunk.Select(i => i.ToString()).ToArray()));
    }
}
于 2012-11-20T23:13:17.903 回答
2

Skip()方法和其他类似方法基本上创建一个占位符对象,实现 IEnumerable,它引用其父可枚举并包含执行跳过的逻辑。因此,循环中的跳过是无效的,因为它们不是像您认为的那样丢弃可枚举的元素,而是添加了一个新的逻辑层,当您真正需要第一个元素之后,它会延迟执行我跳过了。

您可以通过调用ToList()或来解决此问题ToArray()。这迫使对该方法进行“急切”的评估Skip(),并且确实摆脱了您从将要枚举的新集合中跳过的元素。这会增加内存成本,并且需要知道所有元素(因此,如果您在IEnumerable代表无限系列的情况下运行它,祝您好运)。

第二种选择是不使用 Linq,而是使用IEnumerable实现本身来获取和控制IEnumerator. 然后代替Skip(),只需调用MoveNext()必要的次数。

于 2012-11-21T00:11:35.220 回答