Pull to refresh

Comments 56

Попробуйте https://www.nuget.org/packages/MemoryPools.Collections/. По подключении nuget делаете .AsPooling() между источником и LINQ вызовами и всё. Главное чтобы это участвовало бы в местах с вызовом Dispose над IEnumerator. Например, в foreach(). заворачивает экземпляры инстансов классов от Select/Where/… в пул.

Выглядит любопытно) Посмотрим, спасибо!

Лучше особо критичные места итерируйте через for(...) тем более что у вас List много где. Да понимаю что хардкор, но скорость того стоит - мне лично таким способом удалось потребление памяти снизить с 40+ Гб до 10-15 Гб

По скорости, допустим, может и так, но потребление памяти? Не совсем понял, в чём тут будет выигрыш по памяти?
Потому что при использовании foreach создается замыкание и происходит выделение памяти. Никогда не обращали внимания на то, что плагин для R#/Rider показывающий выделения, всегда подчеркивает in в foreach?
сам себе отвечу — нет не всегда при foreach создается замыкание и следовательно не всегда происходит аллокация. Получается ввел в заблуждение

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.
Если я верно понял, то в принципе в статье об этом есть)
да, в статье есть об этом.
отлично конечно, но при использовании for вы совсем не обязаны знать таких тонкостей и при это такой код понятен не то что Junior разработчику, но даже школьнику и при этом он гарантированно быстрый (хотя и страшный конечно)

Впрочем 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) 

Из фора заинлайнился

Проперти в обоих случаях заинлайнились. В случае с forach же нет вызова get_Current() у итератора.

Я бы тоже на это рассчитывал, но потом проверил и оказалось что jit оставил не виртуальный вызов.


Увидеть можно если включить отображения asm-а на sharplab.io в той ссылке что я скидывал.


То есть такая возможность есть, но на практике это не так. Возможно в последующих версиях (или в .Native AOT) завезут ещё оптимизаций)

Но если итерироваться не по параметру метода, а по любому филду, который может быть изменён из другого потока, то for по массиву проигрывает foreach:
пруф посмотрите на ассемблер.

Конечно, потому что foreach первым делом его в локальную переменную сохранит, а for нет. Такого же эффекта можно добиться если самому сохранить его в локальную переменную.

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

Но опять же, в случае с листом всё наоборот, т.к. лишний call стоит дороже.

Мой комментарий был скорее для более полного развёртывания для остальных читателей вот этого тезиса:
В случае массивов, для foreach компилятор просто генерит код как для обычного for.

Потому что в общем виде это не совсем так.

А, тут конечно вы правы, есть корнеркейсы.

Задумался о том, что меня смутило в главе об оптимизации Linq. Если запросы «сотни тысяч и даже миллионы раз», то они как минимум должны кэшироваться, а не проходить каждый раз заново. Но возможно, тут какой-то интересный кейс, где кэшировать невозможно. Хотелось бы подробностей.
Это ведь могут быть разные запросы. К примеру, поиск в ширину, большая часть запросов будут отображать свой уникальный список — чего тут кешировать?
Очень интересно, как Linq-ом делать поиск в ширину?
Эх, даже жаль, что нельзя, видимо(
А жалко, могла бы быть интересная задачка...)

PS Или всё-таки можно???

Простите, уже не вспомню, что имел в виду под поиском в ширину.

Скорее всего имел в виду поиск вхождений по множеству параметров, который сделает бесполезным кеширование из-за большой комбинаторики параметров.

Сейчас тоже не могу представить, как делать поиск в ширину по графу, разве что вспомогательные функции

Запросы выполняются действительно часто, но ведь у них, как правило, разные исходные данные и соответственно — разный результат. Во всяком случае у нас результаты выполнения этих запросов кэшировать не было смысла (хотя мы кэшируем многие другие вещи).

будь это джава я бы попытался использовать какой-то более параллельный сборщик мусора. для шарпа нет такого?

Насколько мне известно, в C# нельзя «поменять» сборщик мусора. Впрочем, если такое вдруг каким-то образом возможно, то я бы хотел узнать, как.

Похоже, у них уже серверный и блокирующий вариант сборки мусора уже включен. (Вариант, что клиентский и блокирующий, так как если загрузка ЦП падает до 15%, это не многопоточный сборщик) Возможно, стоит перейти на неблокирующий?

Насколько я понял, общее время выполнения от этого только увеличится. Серверный он для увеличения производительности, а клиентский — для быстроты реакции.


Им надо, наверное, читать книжку Кокосы и разбираться в том, почему все так долго. Может, у них фрагментация большая или mid life crisis или финализаторов много.

На самом деле у нас используются параметры по умолчанию, но возможно, нам действительно стоит с ними поэкспериментировать, спасибо
На самом деле можно. Они выделили интерфейс для сборщика мусора, когда перешли на .net Core и открыли исходники, но его реализация займёт уйму времени и не факт, что он будет лучше даже на вашей конкретной задаче. Подменить его можно только перед запуском приложения(не через АПИ в рантайм). Можете посмотреть доклад «Konrad Kokosa — Writing a custom, real-world .NET GC» с Dotnext, доступен на Youtube.

p.s. Сам GC хорошо настраивается разными аттрибутами. В первых минутах видео Конрад говорит об этом. Как минимум вам стоит переключить GC в конкурентный серверный режим, если это ещё не сделано.
Сам GC хорошо настраивается разными аттрибутами.

Все-таки настраиваемость и другой GC это разные вещи, например в джаве shenandoah и G1 работают приницпиально по-разному и никакие конфигурации тут не помогут, один будет однозначно выигрывать в одних сценариях, а другой во-вторых.
Интересно почему на джаве есть большое количество GC, встроенных и сторонних, а на .net нету-все-таки сценарии использования языков довольно близки

Хм, это довольно интересно. Конечно, свой GC писать мы явно не станем, но покопать в сторону настройки существующего действительно стоит :)
Для этого придётся PVS-Studio перевести на .NET core?
Для своего кастомного GC — да. Для настройки существующего GC — нет, но в .Net Core этих настроек заметно больше.
Может быть, я не очень внимательно прочитал, но каков выигрыш по производительности и памяти удалось получить после оптимизации энумераторов и LINQ?

По памяти — никакого, выигрыш был в уменьшении частоты выполнения сборки мусора. Временная память, выделяемая итераторам и остальным linq-классам, держалась потоками слишком мало, чтобы повлиять на свап и подобное, но настолько частое выделение-освобождение заставляло приложение чаще запускать GC. А насколько быстрее стало — похоже, несильно, так как после оптимизации кэширования DisplayPart получили 20%, а после оптимизации энумераторов LINQ — "больше 20%".

То есть, получается, менять IEnumerable на List большого практического смысла не было? При том, что на практике это может выстрелить в ситуациях, когда придется вызывать ToList() для соответствия контракту.

Ну а LINQ to Objects и Performance в прицнипе сочетаются так себе обычно. :)
Почему бы в примере с foreach не задать размер результирующего списка равным 5?
Ведь список по умолчанию имеет размер равным 4, и для пятого элемента будет выделен массив размером 8 элементов, в итоге у вас один лишний объект (лист с 4 элементам), лишняя операция копирования массива из 4-х элементов в массив из 8-ми элементов и пустая трата памяти под неиспользуемые 3 элемента
Да, кажется это имеет смысл
самая большая проблема это «выделенная память» не ясно какими инструментами находить эти проблемы и как решать
image
это не нормально когда реальной памяти потребляет 35-60 мб, а выделенная разжирается в 10 в 20 в 30 раз больше и не падает не понятно из за чего.

По второй ссылка инструмент не показывает ничего по выделенной памяти, первую ссылку попробую посмотреть

Почему это не нормально и какая с этим связана проблема?

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

Если я правильно понимаю то "Выделенная память" это просто виртуальная память, то есть адресное пространство, которое операционка вам зарезервировала под указатели, которые потом будут смаплены в реальную физическую память.

Если это так то на 64-х битной операционке крайне сомнительно что бы она у вас могла закончится, даже с учётом что под указатель не все 64 бита используются, там всё равно неприлично большие числа.

И я не могу ничего запустить тяжелого, приложения ругаются что не хватает памяти, я повторю еще раз у меня свободно 30 гб физической памяти

Что то подсказывает что проблема в этом чём то тяжёлом. Возможно аллоцирует большие блоки непрерывной памяти например. И тогда уже проблема будет в фрагментации. Для решения этого есть кстати всякие геймерские тулы. Хотя за их эффективность не поручусь.

А меня вот заинтересовало, как Вы «создали свой тестовый проект с большим количеством сложных конструкций»? Задача вроде-как нетривиальная. Вряд ли для тестов анализатора представляет интерес искуственно нагенерированные for/if/etc конструкции. Полагаю, Вы просто выбрали проект из вашей обширной библиотеки, который имел близкие статистические показатели?

И еще вопрос, а почему было не снять performance/memory снэпшоты прямо с проблемной клиентской машины? У dotTrace/dotMemory есть консольные версии, которые не требуют установки и лицензии — в целом не сильно сложнее, чем взять дамп процесса.
Действительно, гонять проверку и заниматься оптимизацией на клиентском коде было бы проще всего, однако возникают проблемы юридического плана — мало кто отдаст всю свою кодовую базу в стороннюю компанию, а уж сколько документов придётся для этого оформлять.

На самом деле, у нас был пяток исходных файлов от клиента, которые он прислал нам по взаимному согласию. На их основе мы, с помощью специального алгоритма, сгенерировали десятки тысяч похожих фалов — по размеру, концентрации и степени вложенности конструкций ветвления кода, связанности кода через перекрёстные вызовы методов и т.п. Сам Roslyn, на основе которого работает анализатор, позволяет решать такие задачи достаточно удобно. И уже на этом синтетическом проекте мы воспроизвели описанные в статье проблемы.
Понятно, что клиент исходники не отдаст. Но почему не сделать профиляцию на клиентской машине в момент возникновения проблемы? Дамп процесса ведь вам отдали, а снэпшоты dotTrace/dotMemory содержат уж точно не больше sensitive данных.

А насчет генерации тестового проекта, интересно, это ваш собственный внутренний алгоритм? Или что-то за основу взято? (у меня возникала схожая задача, но решил не связываться, показалось слишком трудоемким)
Может быть снэпшоты и можно было бы получить, но и для получения дампа пришлось повозиться с бюрократией, так что вариант с генерацией нам на тот момент показался проще — если бы не получилось воспроизвести там, то возможно пошли бы дальше копать в сторону дампа.

Алгоритм наш собственный.
Sign up to leave a comment.