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

"Я знавал одного разраба, который мог написать запрос абсолютно любой сложности на LINQ, но если надо было что-либо в нём поменять, он писал всё заново" - мой тимлид из EPAM...

Но что если я скажу вам, что с .Net7 всё начнёт меняться. Если вы не были заняты последний год чисткой беклога не поднимая головы, то наверное слышали про новые фичи в .Net6 которые капитально облегчат жизнь разработчикам.

Zip() теперь может совмещать 3 коллекции, ElementAt() теперь может принимать оператор индекса "^" чтобы отсчитывать с конца коллекции, Take() теперь принимает диапазон индексов list.Take(2..6). Также появились новые методы TryGetNonEnumeratedCount(), чтобы не проходиться по коллекции для получения Count, MinBy(), MaxBy(), AvarageBy(), название которых говорит само за себя и новый метод Chunk(), дробящий коллекцию\массив на равные части.

.Net7 же берётся за классические методы и творит с ними чудеса, пока-что особое внимание получили только 4 метода:
.Min()
.Max()
.Average()
.Sum()

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

И результат был, но не капитальный. Память всё равно выделялась и написание таких методов раздувало проект.

По массиву из 100 чисел
По массиву из 100 чисел

Теперь посмотрим на бенчмарк по одной дженерик коллекции но в разных версиях фреймворка. Net6 и Net7 RC1, все те методы, получившие изменения.

Бенчмарк производительности методов в .Net6 и .Net7 RC 1 по одинаковой коллекции. С канала Nick Chapsas.
Бенчмарк производительности методов в .Net6 и .Net7 RC 1 по одинаковой коллекции. С канала Nick Chapsas.

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

Надо разобраться. Быть того не может.

Берём для примера реализацию Min() из .Net6, в принципе она не менялась давно.
Для тех кто внезапно не видел реализацию этого метода, вот кодище:

Впринципе всё ожидаемо и ясно как белый день, метод расширения с классической реализацией, даже до конца можно не показывать...

А вот когда мы смотрим реализацию на .Net7, начинается интересное. Тут многое по другому.

В net70 класс Enumerable сначала определяет с каким типом мы работаем.
В net70 класс Enumerable сначала определяет с каким типом мы работаем.

Мы не сразу переходим в блок кода, мы видим лямбду на метод MinInteger() куда передаётся коллекция, и уже в нём начинается буст производительности.

Это стало возможным поскольку в методы добавили дополнительное условие:

Если мы не можем получить Span<T> из IEnumerable to включается классическое исполнение метода
Если мы не можем получить Span<T> из IEnumerable to включается классическое исполнение метода

Сначала мы пытаемся получить из источника структуру Span<T>. Кто не в курсе, что она делает, советую почитать или посмотреть, объяснять её устройство крайне долго, в двух словах - обьект Span представляет непрерывную область памяти и храниться в стеке а не в хипе.

Если же мы не можем получить Span из источника, то всё идёт по старой схеме. Цикл с поэлементным поиском, как и в старых версиях фреймворка.

Если же же да, то условие запускает новый блок кода с векторизацией поиска:

Если мы можем получить Span<T> то включается векторизация
Если мы можем получить Span<T> то включается векторизация

Когда нам удаётся получить Span<T> из источника, запускается поиск с помощью вектора Vector<T> .

Интерпритация IL.
Интерпритация IL.

Если начать полностью объяснять процесс векторизации то это займет ещё пару тройку мониторов (Ссылка на полное описание процесса векторизации в конце этой статьи.). Поэтому опишем "грубо говоря":

Дополнительное условие проверяет, является ли длинна битов нашего полученного Span такой же или больше чем Vector с тем же переданным типом, но умноженным на 2. Это позволяет нам искать не по значению, а по длинам кусков битов нашей непрерывной области памяти, и затем сравнивать только значения совпавших длин с нужным результатом. В итоге мы сокращаем поиск в десятки, а может и в сотни раз так как шанс того, что элементы массива или коллекции будут повально занимать такое же пространство, что и наше желаемое значение, крайне мал.

Также условие IsHardwareAccelerated проверяет возможно ли ускорение процесса со стороны железа. Чтобы вернулось true мы должны быть на х64 платформе, а также в Release моде.

 bool зависит от того что скажет JIT компилятор
bool зависит от того что скажет JIT компилятор

Однако если даже мы в Debug то уже в рантайме параметр вроде как всё равно измениться на true и векторизация сработает.

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

Почитать про векторизацию можно тут

Структура Vector<T>

Скрин бенчмарка и объяснение работы Span<T> на YouTuve каналe Nick Chapsas

2022. Платон Звонков, начинающий разработчик.

Комментарии (23)


  1. mayorovp
    06.11.2022 14:27
    +2

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


    Вот если бы они общий случай ускорить умудрились...


    1. a-tk
      06.11.2022 14:53
      +1

      Вот если бы они общий случай ускорить умудрились...

      Для этого надо сначала научиться инлайнить лямбды.


  1. mbobka
    06.11.2022 19:45
    +2

    Это не оригинальный пост, а перевод видео с канала Nick Chapsas. Все скриншоты взяты оттуда.


    1. ArkadiyShuvaev
      06.11.2022 19:57
      +6

      Автор-то, молодец, на самом деле. Нашел новую нишу. Перевод популярных Youtube каналов с проф. тематикой.


      1. dabrahabra
        07.11.2022 04:50
        +1

        Типа параллельный импорт? Где ссылка на авторство?


  1. Sektor2350
    06.11.2022 22:48
    +11

    Linq сложно читается... what?


    1. a-tk
      07.11.2022 11:13
      +2

      И уж точно проще регулярных выражений :)


    1. SadOcean
      08.11.2022 00:15

      Да нет, все верно.
      Linq (или например реактивные расширения) очень плотные, обладают специфической логикой и не очень добры к инструментам пояснения, типа нейминга.
      Например можно добавить GroupBy(itemsGroup => Condition(itemsGrout)), но логический смысл группировки будет раскрыт хуже, чем если бы мы писали в обычном процедурном стиле с временными переменными.


      1. Bronx
        08.11.2022 13:29
        -1

        А если написать прощё: .GroupBy(Condition), то смысл будет ещё хуже понятен?


        1. SadOcean
          08.11.2022 17:35

          Это был условный пример.
          Конечно если обернуть в понятное имя - конкретно тут будет неплохо.
          А если в неудачное - то станет даже хуже (нельзя посмотреть условие на месте, нужно идти в метод)


  1. KReal
    07.11.2022 01:36
    +9

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


    Вот честно, первый раз слышу такие претензии. Что касается «медленный и аллоцирует память» — ну если делать lazy цепочки над IEnumerable ничего там аллоцироваться не будет и медленно работать тоже. По поводу сложно читается — во всех наших командах мы как раз можем пренебречь потерями в производительности ради декларативности Linq, которая плюс-минус понятна всем. Про базу данных — ну наверное, если можно писать запросы на Linq (в EF или где там еще), то база это позволяет, а если нужен перфоманс — то велком в plain SQL или прости господи процедуры и иже с ними.
    Вот про джунов согласен — понятное дело, что если на собеседовании просят, например, отсортировать коллекцию, то .Sort() это слишком толстый лайфхак)


    1. Raneddo
      07.11.2022 13:54

      То есть на собеседовании вы просите джуна отсортировать массив? А каким алгоритмом? Если пузырьком, то это вообще бесполезно. Максимум, надо знать О нотацию и основную идею алгоритма. А если вы просите написать qsort, то это вообще нонсенс. Даже Яндекс (и всё, что выше за границей) не просят писать алгоритмы сложнее бинарного поиска, там задачи на подумать. Возможно, у вас компания, которая занимается оптимизацией алгоритмов сортировки, тогда я могу понять такой вопрос, иначе он бесполезен


      1. KReal
        08.11.2022 02:13
        +1

        Ну наверное любым. В интересах собеседуемого предложить разные варианты. Если не qsort, то слиянием, почему нет. Но мне кажется, вы уводите в сторону — я отвечал на «джунов не просят использовать LINQ на собеседованиях». Практическую пользу от линка на собесе я сходу узреть не могу)


  1. Deosis
    07.11.2022 07:37
    +3

    Также появились новые методы TryGetNonEnumeratedCount(), чтобы не проходиться по коллекции для получения Count

    Пример дичайшего легаси. А все потому, что программисты использовали count для выполнения побочных эффектов. И теперь нельзя ускорить этот метод, не сломав кучу пользовательского кода.


    1. Popou
      08.11.2022 11:32

      Прошу прощения, а для чего ещё может использоваться Count?


      1. mayorovp
        08.11.2022 13:13

        Для подсчёта количества, как бы.


        1. Popou
          08.11.2022 13:15

          Да нет, я не об этом, автор коментария говорит о побочных эффекта. Мне вот стали интересны именно они.


          1. mayorovp
            08.11.2022 13:24
            +3

            Ну вот смотрите:


            var hashSet = new HashSet<Foo>();
            foos.Count(hashSet.Add);

            Это ещё более-менее (скорее менее, чем более) нормальный способ, ведь подсчитать число удовлетворяющих предикату объектов невозможно без выполнения этого предиката. Но иногда делали и вот так:


            var list = new List<Bar>();
            foos.Select(foo => {
                list.Add(foo.Bar);
                return foo;
            }).Count();

            Вот это уже абсолютно ненормальный способ, ведь селектор тут вообще исполнять никто не обязан. Но нет, были те кто считал это отличной заменой циклу foreach, ведь они совершенно точно знали, что внутри Count есть именно такой цикл! А теперь уже Microsoft не могут этот цикл убрать из Count…


  1. alexhott
    07.11.2022 08:18

    Вы не любите кошек? Да вы просто их готовить не умеете.
    Перед тем как чего-то написать ан LINQ я довольно долго писал и оптимизировал SQL запросы. По началу пришлось смотреть что в итоге LINQ отправляет серверу и от чего это зависит, оказалось что можно таки выйти на любой оптимальный SQL запрос используя синтаксис LINQ, и мне уже не хотелось писать внутри шарпового кода голые SQL.


    1. ptr128
      07.11.2022 11:38

      Может Вы путаете LINQ (ORM) с Linq2DB (типизированный SQL)?


      1. alexhott
        07.11.2022 13:28

        Ну конкретно мой комментарий конечно относится к работе с БД, а не просто с любыми коллекциями в памяти, да и в статье вроде как LINQ в контексте рядом с SQL. Хотя вот так сразу не вспомню сценария когда коллекция рождалась бы не из БД и с ней нужно было какие-то операции с большим объемом провернуть.


        1. baklajan
          07.11.2022 15:19
          +1

          Много же примеров Linq2: XML, CSV и т.д.

          Да и само понятие большое тоже растяжимое думаю, в каком-то кейсе 1000 штук много, в каком-то сотни миллионов не очень много.


        1. ptr128
          07.11.2022 18:17

          Так я об этом и спросил. Если Linq сначала делает SELECT, предоставляя доступ к данным коду на клиенте, а потом INSERT ... VALUES или SqlBulkCopy, то Linq2DB ориентирована на [WITH ...] INSERT/MERGE ... FROM, когда клиенту данные могут вообще не пересылаться.

          Парадигма разная. Первая - ORM, а вторая - синтаксический сахар для написания SQL в C# и берущая на себя проблемы различия синтаксиса в разных СУБД.