我遇到了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.");
答案 0 :(得分:9)
.Skip()
所做的是创建一个循环在源上的新IEnumerable
,并且只在第一个N
元素之后开始产生结果。你链接谁知道其中有多少相继。每次调用.Any()
时,都需要再次遍历所有先前跳过的元素。
一般来说,在LINQ中设置非常复杂的运算符链并重复枚举它是一个坏主意。此外,由于LINQ是一个查询API,因此当您尝试实现的内容等于修改数据结构时,Skip()
等方法是一个糟糕的选择。
答案 1 :(得分:4)
你有效地将Skip()链接到同一个枚举上。在250的列表中,最后一个块将从一个懒惰的可枚举中创建,前面有~25个'Skip'枚举器类。
如果你做的话,你会发现事情变得更快了
workEnumerable = workEnumerable.Skip(chunk.Count).ToList();
但是,我认为可以改变整个方法。
如何使用标准LINQ实现相同目的:
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()));
}
}
答案 2 :(得分:2)
Skip()
方法和其他类似方法基本上创建一个占位符对象,实现IEnumerable,引用其父可枚举并包含执行跳过的逻辑。因此,循环中的跳过是非高效的,因为它们不是像你认为的那样丢弃可枚举的元素,而是添加了一个新的逻辑层,当你实际需要第一个元素之后,它会被懒惰地执行。我跳过了。
您可以致电ToList()
或ToArray()
来解决此问题。这迫使“急切”评估Skip()
方法,并且确实摆脱了您将要枚举的新集合中跳过的元素。这需要增加内存成本,并且需要知道所有元素(所以如果你在代表无限系列的IEnumerable
上运行它,祝你好运。)
第二个选项是不使用Linq,而是使用IEnumerable
实现本身来获取和控制IEnumerator
。然后,只需拨打Skip()
所需的次数,而不是MoveNext()
。