Почему IEnumerable slow и List работает быстро?

Перешел через этот код.

var dic = new Dictionary<int, string>();
for(int i=0; i<20000; i++)
{
    dic.Add(i, i.ToString());
}

var list = dic.Where(f => f.Value.StartsWith("1")).Select(f => f.Key);//.ToList(); //uncomment for fast results 
Console.WriteLine(list.GetType());
var list2 = dic.Where(f => list.Contains(f.Key)).ToList();
Console.WriteLine(list2.Count())

Итак, когда .ToList() прокомментирует это медленно, а не - быстро. Воспроизводимый здесь Как это можно объяснить? Должен ли я всегда делать все ToList() для обеспечения скорости (т.е. В каких обстоятельствах IEnumerable был бы более предпочтительным)? Заметьте, что я говорю только о linq для объектов, я знаю linq для sql лень и т.д.

Ответ 1

Это из-за отложенного выполнения: при комментировании ToList перечисление создается путем оценки последовательности фильтров для каждого элемента в словаре. Однако, когда вы выполняете ToList, последовательность "материализуется" в памяти, поэтому все оценки выполняются ровно один раз.

Логика второго Where без ToList выглядит следующим образом:

// The logic is expanded for illustration only.
var list2 = new List<KeyValuePair<int,string>>();
foreach (var d in dict) {
    var list = new List<int>();
    // This nested loop does the same thing on each iteration,
    // redoing n times what could have been done only once.
    foreach (var f in dict) {
        if (f.Value.StartsWith("1")) {
            list.Add(f.Key);
        }
    }
    if (list.Contains(d.Key)) {
        list2.Add(d);
    }
}

Логика с ToList выглядит так:

// The list is prepared once, and left alone
var list = new List<int>();
foreach (var f in dict) {
    if (f.Value.StartsWith("1")) {
        list.Add(f.Key);
    }
}
var list2 = new List<KeyValuePair<int,string>>();
// This loop uses the same list in all its iterations.
foreach (var d in dict) {
    if (list.Contains(d.Key)) {
        list2.Add(d);
    }
}

Как вы можете видеть, ToList преобразует программу O(n^2) с двумя вложенными циклами размером n в O(2*n) с двумя последовательными циклами размером n каждый.

Ответ 2

LINQ использует отложенное выполнение.
Если вы не вызываете .ToList(), результаты запроса никогда не сохраняются нигде; вместо этого он повторно повторяет запрос каждый раз, когда вы повторяете результаты.

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

Однако ваш код повторно выполняет итерацию запроса; один раз для каждого вызова обратного вызова Where().

Вы должны заменить эту строку вызовом Join() и no ToList(), который будет быстрее, чем любой из них.

Ответ 3

Потому что, когда у вас нет вызова .ToList(), экземпляр list2 будет перебирать через весь list, перечислимый для каждого элемента в словаре. Итак, вы используете от O (n) до O (n ^ 2), если используете отложенное выполнение.

Ответ 4

Это вызвано отсроченным исполнением. IEnumerable не должен быть статическим набором. В общем случае это некоторый источник данных (dic в вашем случае) + все методы и выражения (Where, Contains и т.д.), Которые приводят к окончательному набору.

.ToList() выполняет все эти методы и выражения и генерирует конечный результат.

Поэтому, если вы используете ToList(), он генерирует стандартный список .NET(массив целых чисел) и выполняет все операции над этим списком.

Если вы не вызываете ToList() (или любой другой метод To), IEnumerable можно перечислить несколько раз.