Comments 56
Прям бальзам на сердце :)
Попробуйте https://www.nuget.org/packages/MemoryPools.Collections/. По подключении nuget делаете .AsPooling() между источником и LINQ вызовами и всё. Главное чтобы это участвовало бы в местах с вызовом Dispose над IEnumerator. Например, в foreach(). заворачивает экземпляры инстансов классов от Select/Where/… в пул.
Лучше особо критичные места итерируйте через for(...) тем более что у вас List много где. Да понимаю что хардкор, но скорость того стоит - мне лично таким способом удалось потребление памяти снизить с 40+ Гб до 10-15 Гб
stackoverflow.com/questions/18552669/memory-allocation-when-using-foreach-loops-in-c-sharp
Foreach can cause allocations, but at least in newer versions .NET and Mono, it doesn't if you're dealing with the concrete System.Collections.Generic types or arrays. Older versions of these compilers (such as the version of Mono used by Unity3D until 5.5) always generate allocations.
The C# compiler uses duck typing to look for a GetEnumerator() method and uses that if possible. Most GetEnumerator() methods on System.Collection.Generic types have GetEnumerator() methods that return structs, and arrays are handled specially. If your GetEnumerator() method doesn't allocate, you can usually avoid allocations.
However, you will always get an allocation if you are dealing with one of the interfaces IEnumerable, IEnumerable, IList or IList. Even if your implementing class returns a struct, the struct will be boxed and cast to IEnumerator or IEnumerator, which requires an allocation.
A quick look at the IL code generated by the compiler, you find that the IL of “foreach” is very similar to “for”. Actually the compiler recognizes that the collection is an array and generates an IL code similar to the “for”.
https://zsalloum.medium.com/performance-of-the-loops-in-c-b2961c6d60d8
Впрочем foreach конечно еще проще :)
просто надо понимать, что foreach сгенерирует оптимальный код, если будет знать конкретны тип.
В коде:
var names = this.GetNames();
foreach(var name in names)
{
Console.WriteLine(name);
}
Вы можете менять List на string[] на enumerable и foreach каждый раз будет генерить оптимальный код без изменения смаого исходного кода цикла.
Это не совсем так. В случае массивов, для foreach компилятор просто генерит код как для обычного for. Но для листов и других абстракций это не так (по крайней мере сейчас на текущем компиляторе).
Заглядывать в IL в этом случае даже не обязательно, можно посмотреть результат и в виде C# кода на sharplab.io (пруф)
А какой для листов будет оптимальный код? Я имел ввиду, что он не будет приводить к IEnumerable, а будет использовать struct энумератор.
Итерирование по List<T> безусловно быстрее чем итерирование по абстрактному IEnumerable<T>. Кроме создания лишней обёртки, IEnumerable приводит к 2 лишним виртуальным вызовам на каждую итерацию (вместо 1-го не виртуального), что может оказать намного более значительный результат при большом количестве элементов.
Хотя самым быстрым вариантом для листов сейчас выглядит всё же обычный for, за счёт отсутствия лишнего call-а метода MoveNext. Забавно что для листов JIT не выкинул баундинг чек за предел цикла, как он это делает с массивами.
P.S.: Но вообще на практике я думаю что разница между for и foreach по List<T> будет столь ничтожна что её даже в молотилке цифр будет не разглядеть.
Вы в рассуждениях не забыли, что вызов невиртуального MoveNext скорее всего будет заинлайнен?
На шарплабе сверху не заинлайнен. А вот
callvirt instance !0 class [System.Private.CoreLib]System.Collections.Generic.List`1::get_Item(int32)
Из фора заинлайнился
Я бы тоже на это рассчитывал, но потом проверил и оказалось что jit оставил не виртуальный вызов.
Увидеть можно если включить отображения asm-а на sharplab.io в той ссылке что я скидывал.
То есть такая возможность есть, но на практике это не так. Возможно в последующих версиях (или в .Native AOT) завезут ещё оптимизаций)
Конечно, потому что foreach первым делом его в локальную переменную сохранит, а for нет. Такого же эффекта можно добиться если самому сохранить его в локальную переменную.
Код на ассемблере смотреть не обязательно, можно переключиться в C# вид, что бы наглядно увидеть разницу.
Но опять же, в случае с листом всё наоборот, т.к. лишний call стоит дороже.
А жалко, могла бы быть интересная задачка...)
PS Или всё-таки можно???
Простите, уже не вспомню, что имел в виду под поиском в ширину.
Скорее всего имел в виду поиск вхождений по множеству параметров, который сделает бесполезным кеширование из-за большой комбинаторики параметров.
Сейчас тоже не могу представить, как делать поиск в ширину по графу, разве что вспомогательные функции
будь это джава я бы попытался использовать какой-то более параллельный сборщик мусора. для шарпа нет такого?
Можно только серверную сборку включить в конфигурации https://docs.microsoft.com/ru-ru/dotnet/core/run-time-config/garbage-collector
Похоже, у них уже серверный и блокирующий вариант сборки мусора уже включен. (Вариант, что клиентский и блокирующий, так как если загрузка ЦП падает до 15%, это не многопоточный сборщик) Возможно, стоит перейти на неблокирующий?
Насколько я понял, общее время выполнения от этого только увеличится. Серверный он для увеличения производительности, а клиентский — для быстроты реакции.
Им надо, наверное, читать книжку Кокосы и разбираться в том, почему все так долго. Может, у них фрагментация большая или mid life crisis или финализаторов много.
p.s. Сам GC хорошо настраивается разными аттрибутами. В первых минутах видео Конрад говорит об этом. Как минимум вам стоит переключить GC в конкурентный серверный режим, если это ещё не сделано.
Сам GC хорошо настраивается разными аттрибутами.
Все-таки настраиваемость и другой GC это разные вещи, например в джаве shenandoah и G1 работают приницпиально по-разному и никакие конфигурации тут не помогут, один будет однозначно выигрывать в одних сценариях, а другой во-вторых.
Интересно почему на джаве есть большое количество GC, встроенных и сторонних, а на .net нету-все-таки сценарии использования языков довольно близки
По памяти — никакого, выигрыш был в уменьшении частоты выполнения сборки мусора. Временная память, выделяемая итераторам и остальным linq-классам, держалась потоками слишком мало, чтобы повлиять на свап и подобное, но настолько частое выделение-освобождение заставляло приложение чаще запускать GC. А насколько быстрее стало — похоже, несильно, так как после оптимизации кэширования DisplayPart получили 20%, а после оптимизации энумераторов LINQ — "больше 20%".
Ведь список по умолчанию имеет размер равным 4, и для пятого элемента будет выделен массив размером 8 элементов, в итоге у вас один лишний объект (лист с 4 элементам), лишняя операция копирования массива из 4-х элементов в массив из 8-ми элементов и пустая трата памяти под неиспользуемые 3 элемента

По второй ссылка инструмент не показывает ничего по выделенной памяти, первую ссылку попробую посмотреть
Почему это не нормально и какая с этим связана проблема?
При этом вот такой кривой .net как на картинке выше занимает почти всю «выделенную память» 61/64ГБ
И я не могу ничего запустить тяжелого, приложения ругаются что не хватает памяти, я повторю еще раз у меня свободно 30 гб физической памяти

Если я правильно понимаю то "Выделенная память" это просто виртуальная память, то есть адресное пространство, которое операционка вам зарезервировала под указатели, которые потом будут смаплены в реальную физическую память.
Если это так то на 64-х битной операционке крайне сомнительно что бы она у вас могла закончится, даже с учётом что под указатель не все 64 бита используются, там всё равно неприлично большие числа.
И я не могу ничего запустить тяжелого, приложения ругаются что не хватает памяти, я повторю еще раз у меня свободно 30 гб физической памяти
Что то подсказывает что проблема в этом чём то тяжёлом. Возможно аллоцирует большие блоки непрерывной памяти например. И тогда уже проблема будет в фрагментации. Для решения этого есть кстати всякие геймерские тулы. Хотя за их эффективность не поручусь.
И еще вопрос, а почему было не снять performance/memory снэпшоты прямо с проблемной клиентской машины? У dotTrace/dotMemory есть консольные версии, которые не требуют установки и лицензии — в целом не сильно сложнее, чем взять дамп процесса.
На самом деле, у нас был пяток исходных файлов от клиента, которые он прислал нам по взаимному согласию. На их основе мы, с помощью специального алгоритма, сгенерировали десятки тысяч похожих фалов — по размеру, концентрации и степени вложенности конструкций ветвления кода, связанности кода через перекрёстные вызовы методов и т.п. Сам Roslyn, на основе которого работает анализатор, позволяет решать такие задачи достаточно удобно. И уже на этом синтетическом проекте мы воспроизвели описанные в статье проблемы.
А насчет генерации тестового проекта, интересно, это ваш собственный внутренний алгоритм? Или что-то за основу взято? (у меня возникала схожая задача, но решил не связываться, показалось слишком трудоемким)
Оптимизация .NET приложения: как простые правки позволили ускорить PVS-Studio и уменьшить потребление памяти на 70%