Производительность LINQ и кэширование

99
01 февраля 2021, 22:50

Есть следующий код:

using System.Diagnostics;
class A
{
    public int Count { get; set; }
}
var rnd = new Random();
var lst1 = new List<A>();
var lst2 = new List<A>();
for (int i = 0; i < 10000; i++)
{
    lst1.Add(new A { Count = rnd.Next(10000) });
    if (i % 3 == 0)
    {
        lst2.Add(new A { Count = rnd.Next(10000) });
    }
}
var sw = new Stopwatch();
for (int i = 0; i < 10; i++)
{
    sw.Start();
    var res1 = lst2.Where(i => !lst1.Select(l => l.Count).Contains(i.Count));
    sw.Stop();
    Console.WriteLine(res1.Count() + " " + sw.Elapsed);
    sw.Reset();
}

Первая итерация цикла for при вычислении res1 всегда выполняется существенно медленнее, чем следующие, т.е. после первой итерации результат LINQ кэшируется? И почему если вынести lst1.Select(l => l.Count) внутри for в отдельную переменную результат будет только хуже в плане скорости?

Answer 1

Как уже написал в комментариях tym32167, одна из причин - JIT-компиляция кода CIL.

То есть при исполнении вот этой строки:

var res1 = lst2.Where(a => !lst1.Select(l => l.Count).Contains(a.Count));

сперва компилируются все эти методы: Where, Select, Contains. Классы List<A>, A и его свойство Count уже использовались выше по коду, поэтому они уже скомпилированы.

Что важно: недостаточно просто вызвать эти методы, например, так:

var list = new List<string>();
var test = list.Where(a => a == null).Select(a => a).Contains(null);

Результат замеров от этого не изменится. Хотя, казалось бы, использованы именно эти методы, но нет, у них другая сигнатура, а именно: с параметром List<string>.

А если взять наш класс A:

var list = new List<A>();
var test = list.Where(a => a == null).Select(a => a).Contains(null);

то это сразу скажется на результате замеров со Stopwatch.

А теперь самое важное. Вот эта строка:

var res1 = lst2.Where(a => !lst1.Select(l => l.Count).Contains(a.Count));

ничего не делает (JIT-компиляция не в счёт). Здесь просто создаётся linq-запрос.

А выполнение этого запроса выполняется при вызове метода Count() в этой строке:

Console.WriteLine(res1.Count() + " " + sw.Elapsed);

Это так называемые lazy evaluation (ленивое вычисление) и deferred execution (отложенное выполнение).

Увеличьте количество итераций (размер списка) на порядок в самом первом цикле:

for (int i = 0; i < 100000; i++)

и это сразу станет заметно на глаз. Паузы между итерациями будут по много секунд, а замеренное время sw.Elapsed - доли миллисекунд.

Добавлю ещё вот что. Всё сказанное относится к Linq To Objects - запросам поверх IEnumerable.

Но есть ещё и Linq To Sql (Entity Framework и т. п.) - запросы поверх IQueryable. Они конструируются на клиенте, а выполняются на сервере. И вот тут уже действуют другие правила. План запросов может кэшироваться на сервере и прочее.

READ ALSO
Командна в командной строке не работает

Командна в командной строке не работает

Набираю в командной строке net use - показывает все сетевые дискиВ моем c# коде - пусто

108
Как вывести заголовок к изображению из БД

Как вывести заголовок к изображению из БД

Всем доброго времени суток! Реализовал загрузку изображений в БД и вывод их во вью, но никак не могу решить проблемуПри выгрузке titla'a к изображениям,...

131
Входная строка имела не верный формат

Входная строка имела не верный формат

Выдает ошибку что входная строка имела не верный форматЕсли ячейка будет пустой или заполнена будет символами то постоянно выдает эту ошибку

120
Как определить, что фигура в Автокаде заштрихована?

Как определить, что фигура в Автокаде заштрихована?

Как можно определить на C#, что круг не заштрихован,а полигон - да? Не нахожу подходящие методы

106