Pull to refresh

Comments 21

В том же Net7 появились возможности в связке sealed-классов с PGO

А можно про это намекнуть ссылками? Что-то Google мало что полезного дает..

Итератор List-а медленнее чем у массива хотя бы потому, что дополнительно выполняется проверка, что коллекция не изменилась между итерациями.

а разве в for цикле не может происходить такая же автопроверка?

//первый поток
for (int i = 0; i<ListInt.Count; i++){
  Colsole.WriteLine(ListInt[i]++);
}

//второй поток
for (int j = 0; j<10; j++){
  ListInt.Add(j);
  Swap(ListInt.Last(),ListInt(j));//поменять местами элементы
}

Разве в первом потоке на очередной итерации не выпадет исключение, что коллекция изменилась?

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

В цикле for из статьи происходит только чтение значений из массива, так что там нет никакой проверки на изменчивость.

В массиве вообще нет проверки на изменчивость. В списке есть внутреннее поле версии, которое увеличивается при любой модификации списка внутри соответствующих методов, итератор (реализация IEnumerator) сохраняет у себя значение этого поля в момент своего создания, а потом сверяет его с текущим значением в списке при каждом Next(). Так, по крайней мере, было реализовано до .NET 5, за последние версии стопроцентно не скажу, потому что лично по исходникам не проверял. Это защитный механизм от сюрпризов с пропуском или повторным чтением итератором элементов в случае изменения коллекции, а поскольку для массива элементы не могут добавляться или удаляться, то там такая защита не нужна.

А можно как-то сказать компилятору "зуб даю, не менялась!"?

Для этого существуют массивы.

Если есть желание извратиться, можно через рефлексию получить приватный list._items и перебирать его.

Было-бы интересно глянуть на результат сравнения их обоих с linq. Когда-то давно linq был очень медленным. Несколько лет назад была статья на хабре, где на массивах он проигрывал for/foreach, а на коллекциях толи сравнялся с ними, толи выигрывал. Интересно как теперь с ним обстоят дела в нет7.

Производительность LINQ сильно привязана к generic-типу коллекции, типу самой коллекции, с которой происходит работа, и к собственно самой функции вызова. Ведь LINQ это не просто перечисление, это и какая-то полезная нагрузка.

Там, например, на .Net7 .Sum() на массивах чисел работает с помощью SIMD. А на каком-то сложном IEnumerable - просто складывает влоб. А в нашем случае сложение выбрано как относительно нейтральная и бесплатная полезная нагрузка. (Будь у нас какой-нибудь супер-умный компилятор, как у C++, то он бы тоже мог заметить возможность переписать код с использованием SIMD). Кстати, это и не только обсуждалось в предыдушей статье про reciprocal throughput (и отдельного внимания там заслуживают ветки в комментариях, например вот эта).

Поэтому рассматривать производительность LINQ в сравнении с перечислением foreach и циклом for в отрыве от функции и типа коллекции просто неправильно. Такое исследование, конечно, интересно, но скатится в перечисление огромного числа случаев. Больше пользы можно извлечь из чтения патчноутов: что конкретно в LINQ в новом .Net'е улучшили.

Сделал сейчас такой эксперимент:

var array = new int[42];
var ilist = array as IList<int>;

var arrayEnumerator = array.GetEnumerator();
var ilistEnumerator = ilist.GetEnumerator();

Console.WriteLine(arrayEnumerator.GetType()); // System.ArrayEnumerator
Console.WriteLine(ilistEnumerator.GetType()); // System.SZGenericArrayEnumerator`1[System.Int32]

Получается, что GetEnumerator() для T[] возвращает разные реализации если вызвать его напрямую, и если сначала массив привести к IList<T>. Стало интересно, какую реализацию при этом использует foreach, потому что первый вариант вообще недженериковый, а значит, а значит, в случае него там может быть оверхед на боксинг-анбоксинг. Но дизассемблером пока что не ковырял.

UFO just landed and posted this here

"железо" пошло в рост в экспоненциальный рост и он продолжается

По-моему, для технологий на текущем уровне развития всё же есть физический предел, и вряд ли рост сейчас идёт по экспоненте. См., напр., https://habr.com/ru/articles/405723/

Так что никогда не поздно подрихтовать алгоритм вечного цикла, чтобы он работал чуточку быстрее ;)

Несомненно, если выбирать язык под задачу с требованием иметь фокус на производительность - выбирать C# не стоит.

Но это не значит, что на C# нельзя писать эффективные и высоконагруженные приложения - ещё как можно.

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

Например, мотивация для таких задач может быть сугубо экономическая. На определённом масштабе кластера даже из C#-приложений могут достигать сотен инстансов (а то и больше). И пара недель работы инженера над оптимизацией такого приложения на условные 10% потребления ресурсов могут сполна окупиться. При этом, рост до такого масштаба - это не повод переписывать приложение на условный C++.

 применять энергии исследований лучше на специальных областях

Я всецело разделяю рационалистический подход. Но всегда остаются материи из разряда "это просто интересно (и полезно)", "это весело (и полезно)", "мне это доставляет удовольствие (и, судя по всему, не только мне)". А ещё, всегда присутствует фактор глубины знания. От знания устройства инструмента, с которым ты работаешь, результат твоей работы с этим инструментом на длительном промежутке становится точно не хуже. А, я уверен, только лучше.

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

UFO just landed and posted this here

В примере ForArray массив находится в филде, поэтому JIT не может быть уверен, что он не изменится на полностью другой массив, и не удаляет boundary check.

Если массив записать в локальную переменную, и далее по ней идти в цикле до Length, то boundary check удаляется. Код цикла for идентичен варианту foreach на .NET 7 (только регистры разные).

    public int M2()
    {
        var arr = array;
        var sum = 0;
        for (var i = 0; i < arr.Length; i++)
        {
            sum += arr[i];
        }
        return sum;
    }
G_M28255_IG03:
       mov      ecx, esi
       add      edi, dword ptr [rax+4*rcx+10H]
       inc      esi
       cmp      edx, esi
       jg       SHORT G_M28255_IG03

В .NET 6 для for IL такой же, а для foreach одна инструкция add заменяется на две mov+add

G_M55486_IG03:
       movsxd   rcx, esi
       mov      ecx, dword ptr [rdi+4*rcx+16]
       add      eax, ecx
       inc      esi
       cmp      edx, esi
       jg       SHORT G_M55486_IG03

Подробнее в Compiler Explorer https://godbolt.org/z/MdEfGeT1E

Спасибо за отличное дополнение!

Sign up to leave a comment.