[优化此项]:慢速LINQ到对象查询

时间:2010-02-08 14:41:37

标签: c# linq performance optimization linq-to-objects

我有这个问题困扰着我;它被封装为一个新的查询运算符,我制作了两个版本,试图看看哪个更好。两者都表现得非常糟糕。

第一次尝试;陈述式

public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
    return source.Any()
        ? source.Take(length).Cons(source.Skip(length).Section(length))
        : Enumerable.Empty<IEnumerable<α>>();
}

第二次尝试:势在必行的“收益率回报”风格

public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
    var fst = source.Take(length);
    var rst = source.Skip(length);

    yield return fst;

    if (rst.Any())
        foreach (var section in rst.Section(length))
            yield return section;
}

事实上,第二次尝试在可读性,组合性和速度方面都更糟糕。

有关如何优化此问题的任何线索?

8 个答案:

答案 0 :(得分:10)

如果我正确地理解了你的问题,那么你正在尝试构建一个枚举器的惰性实现,它将更大的项集合拆分为更小的可枚举项集合。

例如,一百万个数字的序列可以分成“部分”,每个部分只产生100个,你希望它们都懒惰地完成,即。在制作之前不要将100个项目收集到列表中。

首先,你的尝试会多次重复整个集合,这很糟糕,因此存在性能问题。

如果您正在尝试构建纯粹的延迟实现,则应考虑以下问题:

  • 您只想在基础集合上迭代一次
  • 您应该返回重用底层枚举器的枚举数
  • 您需要处理您返回的部分未完全枚举(例如,调用代码只需要这100个项目中的前50个)。

编辑:在我进入简单化解决方案之前,请注意以下几点:

  • 您无法保存每个部分,即。你不能这样做:collection.Sequence(10).ToArray()来得到一系列的部分。
  • 您不能多次枚举每个部分,因为当您这样做时,它会更改隐藏的基础数据结构。

基本上:我的解决方案不是通用的。如果需要,您应该使用@LBushkin关于MoreLinq Batch的评论,我会毫不犹豫地将我的代码放入类库中,它必须是本地需要的地方,或者重命名能够清楚地警告你的问题。


这是一个简单的实现,我很确定这里有bug,所以你可能想看看为edgecases实现大量的单元测试:

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

namespace ConsoleApplication20
{
    class SectionEnumerable<T> : IEnumerable<T>
    {
        private readonly IEnumerator<T> _Enumerator;

        public SectionEnumerable(IEnumerator<T> enumerator, int sectionSize)
        {
            _Enumerator = enumerator;
            Left = sectionSize;
        }

        public IEnumerator<T> GetEnumerator()
        {
            while (Left > 0)
            {
                Left--;
                yield return _Enumerator.Current;
                if (Left > 0)
                    if (!_Enumerator.MoveNext())
                        break;
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public int Left { get; private set; }
    }

    static class SequenceExtensions
    {
        public static IEnumerable<IEnumerable<T>> Section<T>(this IEnumerable<T> collection, int sectionSize)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");
            if (sectionSize < 1)
                throw new ArgumentOutOfRangeException("sectionSize");

            using (IEnumerator<T> enumerator = collection.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    SectionEnumerable<T> enumerable = new SectionEnumerable<T>(enumerator, sectionSize);
                    yield return enumerable;
                    for (int index = 0; index < enumerable.Left; index++)
                        if (!enumerator.MoveNext())
                            yield break;
                }
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var sequence = Enumerable.Range(0, 100);
            var sections = sequence.Section(10);
            foreach (var section in sections)
            {
                Console.WriteLine(
                    String.Join(", ",
                    section.Take(5).ToArray().Select(i => i.ToString()).ToArray()));
            }
            Console.ReadLine();
        }
    }
}

输出:

0, 1, 2, 3, 4
10, 11, 12, 13, 14
20, 21, 22, 23, 24
30, 31, 32, 33, 34
40, 41, 42, 43, 44
50, 51, 52, 53, 54
60, 61, 62, 63, 64
70, 71, 72, 73, 74
80, 81, 82, 83, 84
90, 91, 92, 93, 94

你应该进行单元测试的事情:

  • 空输入集合不产生任何部分
  • 具有恰当数量元素的集合,只生成一个部分
  • 包含多个截面尺寸元素的集合(即10,20,30等数量的截面尺寸为5或10的元素),在所有元素之后不会产生空白部分预期的
  • 它实际上是懒惰的,如果你枚举第一个10元素部分,但只是第二部分的前5个,只有底层集合的前15个元素被枚举

答案 1 :(得分:9)

我怀疑你遇到的问题与枚举最终结果至少是O(n ^ 2)操作的事实有关,可能更糟;我还没有全力以赴。

为什么?好吧,假设你有[1,2,3,4,5,6],你把它分成你认为的{{1,2},{3,4},{5,6}}

这不是你做过的。事实上,你把它分成{取前两个,取前两个然后丢弃它们然后取下两个,取前两个然后丢弃然后再取下两个并丢弃它们然后取第三个两个}

注意一路上的每一步如何重新计算结果?这是因为数组可能在枚举调用之间发生变化。 LINQ旨在让您始终获得最新结果;你写了一个查询,意思是“跳过前四个并迭代接下来的两个”,这正是你得到的 - 一个在你枚举时执行该代码的查询。

原始序列是否足够小且足够快以至于您可以将整个内容读入内存并立即将其全部拆分,而不是试图懒散地这样做?或者,序列是否可索引?如果你得到的只是对序列的前向访问,并且它太大或太慢都无法一次读入内存,那么你可以在这里做很多事情。但是如果你有这两个属性中的一个或两个,那么你可以使它至少是线性的。

答案 2 :(得分:4)

在可能的情况下,我尝试仅在运算符内迭代一次源。如果来源类似于Reverse()运算符的结果,则调用AnyTakeSkip可能会导致很多恶劣的表现。

您的操作员尝试做什么并不完全清楚,但是如果您可以在不多次读取源的情况下执行此操作,那可能会有所帮助 - 尽管这很大程度上取决于输入的内容。

答案 3 :(得分:3)

这是另一种不使用linq的方法,它比你的第二种方法快得多:

 public static IEnumerable<IEnumerable<a>> Section<a>(this IEnumerable<a> source, int length)
        {


            var enumerator = source.GetEnumerator();
            var continueLoop = true;
            do
            {
                var list = new List<a>();
                var index = 0;
                for (int i = 0; i < length; i++)
                {
                    if (enumerator.MoveNext())
                    {
                        list.Add(enumerator.Current);
                        index++;
                    }
                    else
                    {
                        continueLoop = false;
                        break;
                    }
                }
                if (list.Count > 0)
                {
                    yield return list;
                }
            } while (continueLoop);


        }

答案 4 :(得分:1)

这更快吗?它应该是,因为它只需要通过源序列进行一次迭代。

public static IEnumerable<IEnumerable<T>> Section<T>(
    this IEnumerable<T> source, int length)
{
    return source
        .Select((x, i) => new { Value = x, Group = i / length })
        .GroupBy(x => x.Group, y => y.Value);
}

答案 5 :(得分:0)

我今天有个主意;看看这个

public static IEnumerable<α> Take<α>(this IEnumerator<α> iterator, int count)
{
    for (var i = 0; i < count && iterator.MoveNext(); i++)
        yield return iterator.Current;
}

public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerator<α> iterator, int length)
{
    var sct = Enumerable.Empty<α>();
    do
    {
        sct = iterator.Take(length).ToArray();
        if (sct.Any())
            yield return sct;
    }
    while (sct.Any());
}

这仍然不是超级优雅,但至少实现非常简短和可读。

调查IEnumerator上的查询运算符可能非常有趣。

为方便起见

public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
    using (var iterator = source.GetEnumerator())
        foreach (var e in iterator.Section(length))
            yield return e;
}

答案 6 :(得分:0)

您是否因为某些原因需要保持原始来源?如果没有,为什么不使用递归并使用hd :: tl样式来拉头,将tl传递给递归调用,并且在任何偶数递归合并你坐在一起的两个部分?

通过实验性Ix扩展程序的更新版本,您可以使用WindowBuffer运算符创建sliding window,这应该可以实现您的目标。

答案 7 :(得分:0)

扩展方法怎么样

public static class IEnumerableExtensions
{
    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        List<T> toReturn = new List<T>();
        foreach(var item in source)
        {
                toReturn.Add(item);
                if (toReturn.Count == max)
                {
                        yield return toReturn;
                        toReturn = new List<T>();
                }
        }
        if (toReturn.Any())
        {
                yield return toReturn;
        }
    }
}

一些测试:

[TestFixture]
public class When_asked_to_return_items_in_sets
{
    [Test]
    public void Should_return_the_correct_number_of_sets_if_the_input_contains_a_multiple_of_the_setSize()
    {
        List<string> input = "abcdefghij".Select(x => x.ToString()).ToList();
        var result = input.InSetsOf(5);
        result.Count().ShouldBeEqualTo(2);
        result.First().Count.ShouldBeEqualTo(5);
        result.Last().Count.ShouldBeEqualTo(5);
    }

    [Test]
    public void Should_separate_the_input_into_sets_of_size_requested()
    {
        List<string> input = "abcdefghijklm".Select(x => x.ToString()).ToList();
        var result = input.InSetsOf(5);
        result.Count().ShouldBeEqualTo(3);
        result.First().Count.ShouldBeEqualTo(5);
        result.Last().Count.ShouldBeEqualTo(3);
    }
}