Как стать автором
Обновить

Комментарии 28

Спасибо за статью. А не пробовали экспериментировать с режимом работы GC? Планируете ли опубликовать получившиеся «кусочные» реализации коллекций?
Мы сравнивали режимы работы сборщика мусора GCLatencyMode.Interactive и GCLatencyMode.SustainedLowLatency. На замерах разница в поведении вызывалась многими факторами, например количеством запросов, объемом памяти, уже используемым приложением. На фоне этих различий влияние режима не заметно.
Если же обратиться к документации (https://docs.microsoft.com/en-us/dotnet/api/system.runtime.gclatencymode?view=netframework-4.8), то отличие SustainedLowLatency в том, что он старается не выполнять блокирующую сборку 2-го поколения. Наши замеры показывают, что в веб-сервисах Pyrus происходят только фоновые сборки мусора, а следовательно, режим SustainedLowLatency не должен дать никаких изменений.
Вот так у нас практика сходится с теорией.
По поводу публикации «кусочных» коллекций — мы над этим думаем. Если есть интерес, то почему бы и нет.
Отличная статья, большое спасибо, теперь можно не писать такую для коллег =)
По теме — нам кроме собственно уменьшения количества выделяемой памяти и объектов помог GCLatencyMode.SustainedLowLatency. Heap у нас с вами сопоставимого размера, выделений у нас раза в полтора поменьше.
Спасибо! Рады, что статья оказалась полезной. Про режимы мы написали в другом комментарии.
В языках типа C/C++ или Rust используется ручное управление памятью, поэтому программисты тратят больше времени на написание кода, управление временем жизни объектов, а затем на отладку.

Вообще-то в Rust автоматическое управление памятью и за соответствием времен жизни ссылок и объектов следит компилятор.

я так понял, borrow checker заставляет следить программиста ;)

Снимаю шляпу, прекрасная статья :)
Спасибо!
Интересно, можно организовать два независимых дублирующих процесса, чтобы когда один приостановился, то второй обрабатывает запросы? Можно даже на разных компьютерах
Я слышал, что кто-то именно так и делал. Можно подписаться на событие сборщика мусора, которое он кидает незадолго до сборки: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/notifications. По этому событию сервер посылает сообщение балансировщику нагрузки, и тот временно перестает распределять запросы на этот сервер. Но это экзотика, редко используется.
А сколько времени заняло от «Вооружившись недавно вышедшей книгой» до «Сборки 2-го поколения стали быстрее, длинные паузы до 1000мс полностью ушли»?
В статье описан результат работы почти двух месяцев неполной занятости. Это итеративный процесс: замер, исправление, деплой, повторный замер. Пока ждем деплоя и результатов замера, занимаемся другими задачами.
Посмотрел как себя ведет ArrayPool, создает объект и держит его, давая другим использовать его, если объект заблокирован каким то потоком, то создает еще. Лучше в этом направлении было копать, так вы просто завуалировали объект который среда бы положила в LOH, думаю не спроста она это делает. Похоже на костыли.
Возможно, с ArrayPool-ом правильнее, но сложнее:
1) Поверх ArrayPool все равно придется писать реализацию List и HashSet.
2) Добавляется ручное управление временем жизни: массивы, полученные из ArrayPool-а, надо возвращать обратно.
Сомневаюсь что «поверх» получится. Используйте его подход. Думаю лучше использовать перечислимое обобщение. Надо глянуть что там в исходниках, возможно сам попробую написать интереса ради.
полученные из ArrayPool-а, надо возвращать обратно.

Операция «возврата» по-видимому, просто снимает блокировку выданного массива и позволяет другим его взять.
Уже в который раз вижу как Newtonsoft.Json является причиной просадки производительности. Жаль конечно что сейчас от него отказаться не так просто- много библиотек от Microsoft его используют. Надеюсь что выпилят его, как и обещали.
Есть альтернатива лучше?
github.com/neuecc/Utf8Json вот этот поинтереснее, имхо. Аллокаций меньше точно, скорость аналогична Newtonsoft.Json (чуть лучше на самом деле, но в пределах нескольких %). Пробовал заменить форматеры в asp.net на него — дали повышение rps ~ на 5-10%.
Ниже\выше уже привели примеры, но могу отметить что в high-load зачастую используют кастомные оптимизации- у меня был опыт написания очень простенького сериализатора «на коленке» для выплевывания geoJSON в мир. Ни одна general purpose библиотека не сможет это поделку обогнать, т.к. сериализатор по сути заточен на один тип входных данных и способен заранее оптимизировать потребление памяти.
Не в обиду Newtonsoft все это сказано — я сам даже контрибутал в него, но уж больно он «зарос» фичами за свою историю, в ущерб производительности.
В .net core 3 — его уже в базе нет. Но без него даже на preview 5 работать нормально сложно. System.Text.Json, а точнее видимо форматер на его основе, еще имеет серьезные баги, типа не поддерживает различные *case, кроме CamelCase.
да, в курсе. к сожалению в Enterprise мире не всегда можно так просто взять и использовать новый фреймворк
Классная статья, которая кроме всего прочего замечательно иллюстрирует отличие языков с ручным управлением памяти и со сборщиком мусора

в первых программист тратит время на дизайн и отладку логики времени жизни объектов

а во вторых программист тратит время на попытки заставить капризный GC делать то, что нужно :)

(Что так-то гораздо сложнее, чем вставить куда надо shared_ptr, а куда надо — weak_ptr на С++).
Я бы сказал, что во-вторых, программист откладывает проблему управлением памятью на неопределенный срок. И во многих проектах (большинстве) этот срок никогда не наступает. :)
В «языках с ручным управлением памятью» тоже не получится «вставить куда надо shared_ptr».
Например очень неприятная проблема с фрагментацией адресного пространства (и нет, на 64-битных системах она не исчезает, а просто отложенно проявляется в виде странного падения производительности). Или кажущееся произвольным падение производительности выделения памяти (особенно в многопоточных приложениях).
Написал «неаккуратный» десериализатор JSON для относительно нагруженного многопоточного сервиса, не использовал пулинг и правильный аллокатор и всё, приехали, утечек памяти нет, а процесс через неделю сожрал несколько десятков гигов, а latency подскочила в 500 раз.
Резюме: если объектов выделяется мало, на производительность условно наплевать (особенно на long tail latency), либо процесс работает недолго — да, можно не беспокоиться. Но обычно в таком случае, можно не беспокоиться и со сборщиком мусора =)

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

Ну принято же считать, что если есть GC — то можно даже и не задумываться и извозчик довезёт


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

Реализация. Есть ArrayPool от Microsoft, но нам нужны еще List и HashSet. Мы не нашли какой-нибудь подходящей библиотеки, поэтому классы пришлось бы реализовывать самим.

Для реализации ListPool и HashSetPool вы можете использовать библиотеку Microsoft.Extensions.ObjectPool. Пример реализации ListPool есть в юнит-тестах этой библиотеки.
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории