[Оптимизируйте это]: замедлите LINQ к запросу объектов

У меня есть этот запрос, который беспокоит меня; это инкапсулируется как новый оператор запроса, я сделал две версии из него, пытаясь видеть, какой работает лучше. Оба работают ужасно.

Первая попытка; декларативный стиль

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;
}

На самом деле вторая попытка хуже, и с точки зрения удобочитаемости, compositionality и с точки зрения скорости.

Какие-либо подсказки относительно того, как оптимизировать это?

8
задан Bent Rasmussen 19 February 2010 в 08:52
поделиться

8 ответов

Я подозреваю, что проблема, с которой вы столкнулись, связана с тем, что перечисление конечного результата является, по крайней мере, операцией O(n^2), а возможно, и хуже; я еще не все продумал в своей голове.

Почему это так? Ну, предположим, у вас есть [1, 2, 3, 4, 5, 6], и вы разбиваете их на то, что вы считаете { { 1, 2 }, {3, 4}, {5, 6}

Это не то, что вы сделали. На самом деле вы разбили это на { взять первые два, взять первые два и отбросить их, а затем взять следующие два, взять первые два и отбросить их, а затем взять следующие два и отбросить их, а затем взять третьи два }

Заметили, как каждый шаг на этом пути пересчитывает результат? Это потому, что массив может меняться между обращениями к перечислению. LINQ был разработан, чтобы всегда получать актуальные результаты; вы пишете запрос, который означает "пропустить первые четыре и итерировать следующие два", именно это вы и получите - запрос, выполняющий этот код при перечислении.

Является ли исходная последовательность достаточно маленькой и быстрой, чтобы вы могли считать ее целиком в память и разбить на части сразу, а не пытаться сделать это лениво? В качестве альтернативы, является ли последовательность индексируемой? Если вы получаете только прямой доступ к последовательности, и она слишком велика или медленна, чтобы прочитать ее в память всю сразу, тогда вы мало что можете сделать. Но если у вас есть одно или оба этих свойства, то вы можете сделать это по крайней мере линейно.

9
ответ дан 5 December 2019 в 06:09
поделиться

Везде, где это возможно, я пытаюсь выполнить итерацию по источнику только один раз внутри оператора. Если источник похож на результат оператора Reverse () , вызов Any , Take и Skip может вызвать множество неприятная производительность.

Не совсем понятно, что пытается сделать ваш оператор, но если вы можете сделать это, не читая источник несколько раз, это вполне может помочь - хотя это будет во многом зависеть от того, что вводится.

4
ответ дан 5 December 2019 в 06:09
поделиться

Вот еще один подход, не использующий 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);


        }
3
ответ дан 5 December 2019 в 06:09
поделиться

Если я правильно понял ваш вопрос, вы пытаетесь создать ленивую реализацию перечислителя, который разбивает большую коллекцию элементов на меньшие перечисляемые коллекции элементов.

Например, последовательность из миллиона чисел может быть разбита на «секции», каждая из которых дает только 100 из них, и вы хотите, чтобы все это делалось лениво, т.е. не собирать 100 предметов в список перед их созданием.

Во-первых, ваши попытки будут повторяться по коллекции много раз, что плохо, отсюда и проблема с производительностью.

Если вы пытаетесь создать чисто ленивую реализацию, вам следует рассмотреть следующие проблемы:

  • Вы хотите выполнить итерацию по базовой коллекции только один раз
  • Вы должны возвращать перечисления, которые повторно используют базовый перечислитель
  • Вам необходимо позаботиться о том, чтобы возвращаемый вами раздел не был полностью перечислен (например, вызывающему коду требовались только первые 50 из этих 100 элементов).

Правка : Прежде чем я перейду к своему упрощенному решению, вот несколько предостережений по этому поводу:

  • Вы не можете сохранить каждый раздел на потом, т.е. вы не можете сделать: collection.Sequence (10) .ToArray () , чтобы получить массив разделов.
  • Вы не можете перечислять каждый раздел более одного раза, потому что, когда вы это делаете, это меняет скрытую базовую структуру данных.

В основном: Мое решение не общего назначения . Вы должны пойти с комментарием @LBushkin о MoreLinq Batch, если вам это нужно, и я бы не стал помещать свой код в библиотеку классов, он должен быть локальным, где он необходимо или переименовано во что-то, что четко предупреждает вас о проблемах с ним.


Вот упрощенная реализация, я почти уверен, что здесь есть ошибки, так что вы можете посмотреть на реализацию тонны модульного тестирования для пограничных ситуаций:

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 элементов базовой коллекции перечислены в
10
ответ дан 5 December 2019 в 06:09
поделиться

Это быстрее ? Так и должно быть, потому что для этого требуется только одна итерация исходной последовательности.

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);
}
1
ответ дан 5 December 2019 в 06:09
поделиться

У меня сегодня появилась идея, посмотрите

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;
}
0
ответ дан 5 December 2019 в 06:09
поделиться

Нужно ли вам по какой-то причине сохранять исходный текст по мере продвижения? Если нет, то почему бы вам не использовать рекурсию и использовать стиль hd :: tl для извлечения головы, передавать tl в рекурсивный вызов, и при любой четной рекурсии объединять две имеющиеся секции в одну?

С обновленным выпуском экспериментальных расширений Ix вы можете использовать операторы Window или Buffer для создания скользящего окна, что должно достичь того, что вам нужно.

0
ответ дан 5 December 2019 в 06:09
поделиться

Как насчет метода расширения

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);
    }
}        
0
ответ дан 5 December 2019 в 06:09
поделиться
Другие вопросы по тегам:

Похожие вопросы: