Comments 21
В том же Net7 появились возможности в связке sealed-классов с PGO
А можно про это намекнуть ссылками? Что-то Google мало что полезного дает..
Рекомендую ознакомиться со всеми статьями целиком, но я приведу ссылки на абзацы именно про sealed-классы:
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#peanut-butter
https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#analyzers
Итератор 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));//поменять местами элементы
}
Разве в первом потоке на очередной итерации не выпадет исключение, что коллекция изменилась?
Чтобы она происходила, её надо написать. Если Вы её не написали, то надо смотреть упоминания поля _version
в исходнике:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/collections/generic/list.cs
Ответ отрицательный.
Полагаю, комментарий был относительно того, что List - это тот же массив, но в котором добавлены методы, как пример, проверки на переполнение и, в случае необходимости, увеличение массива. И это происходит на каждой итерации цикла. Массив же, является основой, которая ничего не делает, кроме непосредственно тех задач, которые на него возложены в конкретной команде.
В цикле for из статьи происходит только чтение значений из массива, так что там нет никакой проверки на изменчивость.
В массиве вообще нет проверки на изменчивость. В списке есть внутреннее поле версии, которое увеличивается при любой модификации списка внутри соответствующих методов, итератор (реализация IEnumerator
) сохраняет у себя значение этого поля в момент своего создания, а потом сверяет его с текущим значением в списке при каждом Next()
. Так, по крайней мере, было реализовано до .NET 5, за последние версии стопроцентно не скажу, потому что лично по исходникам не проверял. Это защитный механизм от сюрпризов с пропуском или повторным чтением итератором элементов в случае изменения коллекции, а поскольку для массива элементы не могут добавляться или удаляться, то там такая защита не нужна.
А можно как-то сказать компилятору "зуб даю, не менялась!"?
Для этого существуют массивы.
Если есть желание извратиться, можно через рефлексию получить приватный list._items и перебирать его.
Можно вытащить Span
с помощью https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.collectionsmarshal.asspan?view=net-7.0
Было-бы интересно глянуть на результат сравнения их обоих с 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
, потому что первый вариант вообще недженериковый, а значит, а значит, в случае него там может быть оверхед на боксинг-анбоксинг. Но дизассемблером пока что не ковырял.
"железо" пошло в рост в экспоненциальный рост и он продолжается
По-моему, для технологий на текущем уровне развития всё же есть физический предел, и вряд ли рост сейчас идёт по экспоненте. См., напр., https://habr.com/ru/articles/405723/
Так что никогда не поздно подрихтовать алгоритм вечного цикла, чтобы он работал чуточку быстрее ;)
Несомненно, если выбирать язык под задачу с требованием иметь фокус на производительность - выбирать C# не стоит.
Но это не значит, что на C# нельзя писать эффективные и высоконагруженные приложения - ещё как можно.
И это не значит, что в задачах, решаемых на C#, да и любом другом языке с фокусом на удобстве, безопасности и скорости процесса разработки, не могут возникать задачи оптимизации.
Например, мотивация для таких задач может быть сугубо экономическая. На определённом масштабе кластера даже из C#-приложений могут достигать сотен инстансов (а то и больше). И пара недель работы инженера над оптимизацией такого приложения на условные 10% потребления ресурсов могут сполна окупиться. При этом, рост до такого масштаба - это не повод переписывать приложение на условный C++.
применять энергии исследований лучше на специальных областях
Я всецело разделяю рационалистический подход. Но всегда остаются материи из разряда "это просто интересно (и полезно)", "это весело (и полезно)", "мне это доставляет удовольствие (и, судя по всему, не только мне)". А ещё, всегда присутствует фактор глубины знания. От знания устройства инструмента, с которым ты работаешь, результат твоей работы с этим инструментом на длительном промежутке становится точно не хуже. А, я уверен, только лучше.
В примере 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
Сказка про For vs Foreach