Насколько эффективны цепочечные операторы LINQ? - PullRequest
3 голосов
/ 01 мая 2020

У меня есть случай, когда я хочу перебрать все, кроме последних 2 элементов коллекции.

Допустим, я делаю это странным образом, как x.Reverse().Skip(2).Reverse().

Будет ли каждый LINQ Операция генерирует эффективно вложенный итератор или вызывает перечисление и т. д. c или это умнее? Что происходит под прикрытием в таком случае?


Разъяснение: это только один пример цепочечных операторов LINQ, которые вы можете увидеть, когда разработчик предпочитает короткий мощный код, не задумываясь о производительность - возможно, они - студент информатики, и это кажется «самым умным» решением. Я не спрашиваю, как решить этот конкретный пример

Ответы [ 3 ]

2 голосов
/ 02 мая 2020

Прежде всего, да, это создает "итератор" и фактически не выполняет итерацию, пока вы не материализовали запрос в foreach или не вызвали ToList для него. Когда вы делаете это, количество итераций, которые происходят, основано на базовом типе. Reverse создаст буферный массив для любого источника, который вы ему дадите, и переберите его в обратном направлении. Если источником является ICollection<T>, то он будет использовать свой метод CopyTo для заполнения массива, что обычно должно приводить к единой массовой копии непрерывных данных в постоянное время. Если это не ICollection<T>, он будет перебирать источник в буфере, а затем перебирать его в обратном направлении. Имея это в виду, вот что происходит с вашим конкретным c запросом при итерации.

Сначала последний Reverse начнет итерировать свой источник (который не является ICollection<T>).

Затем Skip начнет итерацию своего источника

Тогда первый обратный будет либо делать CopyTo, если его источником является ICollection<T>, либо он будет перебирать источник в буферный массив, размер которого изменяется как Нужен.

Затем первый реверс будет перебирать свой буферный массив в обратном направлении

Затем Пропуск получит результаты, пропустив первые два и получив остальные

Затем последний реверс возьмет результат и добавит их в свой буферный массив и при необходимости изменит его размер.

Наконец, последний метод Reverse будет повторять буферный массив в обратном направлении.

Так что, если вы имеете дело с ICollecion<T> это одна CopyTo, а затем 1 итерация всех значений, а затем 1 итерация всех значений, кроме 2. Если это не ICollection<T>, это в основном 3 итерации значений (на самом деле последняя итерация всего, кроме 2). И так или иначе, в процессе также используются два промежуточных массива.

И чтобы доказать, что запрос не выполняет итераций, пока вы его не материализуете, вы можете проверить этот пример

void Main()
{
    var query = MyValues().Reverse().Skip(2).Reverse();
    Console.WriteLine($"After query before materialization");
    var results = query.ToList();
    Console.WriteLine(string.Join(",", results));
}

public IEnumerable<int> MyValues()
{
    for(int i = 0; i < 10; i ++)
    {
        Console.WriteLine($"yielding {i}");
        yield return i;
    }
}

, который производит output

After query before materialization
yielding 0
yielding 1
yielding 2
yielding 3
yielding 4
yielding 5
yielding 6
yielding 7
yielding 8
yielding 9
0,1,2,3,4,5,6,7

По сравнению с другим примером, который у вас был x.Take(x.Count() - 2), он будет выполнять итерацию источника до того, как вы материализуете его один раз для Count (если только это не ICollection или ICollection<T>, в этом случае он просто будет использовать свойство Count), затем он будет повторять его снова, когда вы его материализуете.

Вот тот же пример с другим кодом и полученным результатом.

void Main()
{
    var x = MyValues();
    var query = x.Take(x.Count() - 2);
    Console.WriteLine($"After query before materialization");
    var results = query.ToList();
    Console.WriteLine(string.Join(",", results));
}

public IEnumerable<int> MyValues()
{
    for(int i = 0; i < 10; i ++)
    {
        Console.WriteLine($"yielding {i}");
        yield return i;
    }
}

С вывод

yielding 0
yielding 1
yielding 2
yielding 3
yielding 4
yielding 5
yielding 6
yielding 7
yielding 8
yielding 9
After query before materialization
yielding 0
yielding 1
yielding 2
yielding 3
yielding 4
yielding 5
yielding 6
yielding 7
0,1,2,3,4,5,6,7

То, какой из них лучше, полностью зависит от источника. Для ICollection<T> или ICollection предпочтительнее были бы Take и Count (если источник не может измениться между моментом создания запроса и его материализацией), но если он не является ни тем, ни другим, вы можете предпочесть двойное Reverse чтобы избежать повторения источника дважды (особенно, если источник может меняться между созданием запроса и его материализацией, поскольку размер также может измениться), но это должно быть взвешено с увеличением общего числа выполненных итераций и объема памяти использование.

1 голос
/ 03 мая 2020

Всякий раз, когда вы хотите узнать, как работает оператор LINQ, насколько он эффективен, было бы неплохо взглянуть на исходный код (google: ссылочный источник Enumerable reverse)

Здесь вы обнаружите, что как только вы начнете перечислять вашу последовательность (то есть: используйте метод без отсрочек = используйте метод LINQ, который не возвращает IEnumerable, или используйте foreach), первый метод Reverse будет перечислять вашу полную последовательность один раз , но это в буфере, и начать итерацию назад от последнего элемента.

Ваш skip (2) будет перечислять только 2 элемента.

2-й обратный создаст новый буфер, содержащий эти два элементы и начать итерацию в обратном направлении: так в исходной последовательности вперед.

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

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

Если у вас есть другие операторы LINQ, которые вас интересуют, как это делается, посмотрите на исходный код

1 голос
/ 01 мая 2020

Большинство операций LINQ не производят отдельную вложенную итерацию. Хотя Count() должен повторять всю последовательность.

Что касается содержания вашего вопроса, пожалуйста, обратитесь к: Как взять все, кроме последнего элемента в последовательности, используя LINQ?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...