- PVSM.RU - https://www.pvsm.ru -

Оптимизация по памяти: сложно, но порой необходимо

В преддверии DotNext 2017 мы поговорили со специалистом по оптимизации в том числе .NET приложений из компании JetBrains Андреем Акиньшиным. На конференции он будет рассказывать о том, как отслеживать и устранять различные проблемы работы с памятью, как общего характера, так и специфичные для .NET. В качестве предисловия к докладу мы поговорили о том, какое место оптимизация по памяти вообще занимает в борьбе за производительность приложения.

Оптимизация по памяти: сложно, но порой необходимо - 1


— Расскажите о себе и своей работе. Какую роль в вашей работе играют оптимизации по памяти?

Андрей Акиньшин: Меня зовут Андрей Акиньшин, и я работаю в компании JetBrains, где много времени уделяю оптимизации приложений. Среди прочего мы занимаемся разработкой кроссплатформенной .NET IDE под названием Rider [1], основанной на платформе IntelliJ и ReSharper.
Это очень большой продукт, который много чего умеет. Естественно, он должен быть хорошо оптимизирован с точки зрения работы с памятью. Нужно сделать так, чтобы работа с памятью не тормозила продукт, а расход этой памяти был как можно меньше. Для подобной оптимизационной работы надо хорошо понимать, как эта самая память устроена и какие проблемы с ней могут возникнуть.

В свободное время я также разрабатываю проект с открытым исходным кодом BenchmarkDotNet [2]. На текущий момент он уже достаточно большой, разработка проходит при поддержке .NET Foundation. Эта библиотека помогает людям писать бенчмарки, замеряющие производительность тех или иных вещей.

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

— На ваш взгляд, на каком месте в процессе оптимизации вообще должна стоять работа с памятью? Или все индивидуально и зависит от деталей приложения?

Андрей Акиньшин: Конечно же, многое зависит от приложения. Очень важно понимать узкое место в производительности конкретно вашей программы. Например, если вы много работаете с базами данных, вполне вероятно, что узкое место — это база данных, т.е. в первую очередь нужно думать о ней. А может быть, вы 99% времени тратите на сетевые операции. Всегда необходимо понимать, из чего в принципе складывается ваша производительность.

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

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

— Почему бенчмаркинг памяти такой сложный?

Андрей Акиньшин: Дело в том, что если мы два раза померяем время работы одной и той же программы, мы получим два разных результата. Выполнив очень много замеров, мы получим какое-то распределение с определенной дисперсией. И в случае работы с основной памятью эта дисперсия оказывается достаточно большой. Чтобы улучшить программу с точки зрения производительности по доступу к памяти, нужно очень хорошо понимать, как выглядит это распределение и что может независимо от нашей непосредственной логики влиять на итоговую скорость работы программы.

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

— В своем докладе на DotNext [3] вы собираетесь рассказывать о большом количестве низкоуровневых вещей. Действительно ли .NET-программистам нужно понимать нюансы устройства CPU для оптимизационных работ?

Андрей Акиньшин: В большинстве случаев — нет. Основные перфоманс-проблемы — не особо интеллектуальные, их можно решать, используя общие знания. Но когда простые проблемы решены, производительность все еще упирается в память, а скорость работы надо как-то увеличивать, низкоуровневые знания лишними не будут.

Начну я со знакомой многим вещи — с кэша процессора. Очень важно писать алгоритмы, которые достаточно дружественны к кэшу. Это не так сложно и не требует каких-то больших знаний, а по скорости можно выгадать очень много.
К сожалению, многие профайлеры не позволяют просто так получить количество промахов кэша (cache miss). Но есть специализированные инструменты, позволяющие смотреть именно хардварную информацию. Я использую Intel VTune Amplifier, это очень хороший инструмент — он показывает проблемы не только с кэшем, но и с другими вещами, например, с выравниванием (если у вас много доступа к невыровненным данным, из-за этого может проседать производительность).

Есть такие штуки, о которых многие не знают или не задумываются, — например store forwarding или 4K aliasing: они могут легко испортить наши бенчмарки и привести к некорректным выводам. Мы можем получить проседание по скорости, просто обратившись одновременно по двум адресам, расстояние между которыми оказалось «неудачным». Оптимизация по этим вещам вряд ли даст вам какой-то гигантский прирост по перформансу, но зато может помочь там, где уже ничего другое не поможет. А непонимание внутренней кухни может легко привести к написанию некорректных бенчмарков. Поэтому полезно уметь смотреть и анализировать хардварные счетчики и делать выводы о том, в какую сторону стоит продолжать оптимизационные работы.

— Сейчас очень многие говорят о разработке кроссплатформенных .NET-приложений. Есть ли какие-нибудь различия между работой с памятью под разными рантаймами и операционными системами?

Андрей Акиньшин: Разумеется. Главным образом, это высокоуровневые проблемы, связанные со сборкой мусора. Например, алгоритмы сборки мусора под Mono и полным фреймворком совершенно разные. Идет разная работа с поколениями, по-разному обрабатываются большие объекты.

Кстати говоря, большие объекты — это довольно распространенная проблема. В полном фреймворке у нас есть куча больших объектов (Large Object Heap, LOH), в которую попадают объекты, размер которых превышает 85000 байт. И если обычную кучу сборщик мусора постоянно проверяет, вычищает и дефрагментирует, то над кучей больших объектов операция дефрагментации проводится достаточно редко (по умолчанию она вообще не проводится, но в последних версиях фреймворка ее можно вызвать вручную, если вы считаете, что такая необходимость есть). Поэтому с этой кучей нужно работать очень аккуратно: следить, чтобы у нас больших объектов по возможности не появлялось.

А под Mono у нас уже имеется не 3 поколения, а 2; концепт больших объектов там тоже есть, но работа с ним ведется совершенно иначе. И наши старые эвристики, которые мы применяли для больших объектов под Windows, на Linux под Mono работать не будут.

Оптимизация по памяти: сложно, но порой необходимо - 2

— Расскажите, пожалуйста, о какой-нибудь проблеме из продакшн?

Андрей Акиньшин: В Rider-е есть много проблем с той же кучей больших объектов.
Если мы создаем очень большой массив (который попадает в LOH) на не очень большой промежуток времени, это не очень хорошо, т.к. увеличивает фрагментированность LOH. Классическим решением в этой ситуации является создание вспомогательного класса — так называемого чанк-листа (chunk list): вместо выделения большого массива мы создаем несколько маленьких (чанков), каждый из которых достаточно мал, чтобы не попасть в LOH. Снаружи мы оборачиваем их в красивый интерфейс, чтобы для пользователя они выглядели как единый список. Это спасает нас от большого LOH и дает приятный выигрыш по расходу памяти.

Подобное решение у нас в ReSharper используется достаточно давно. Однако сейчас, когда мы пишем Rider (т.е. по сути запускаем ReSharper на Mono под Linux), этот хак не работает по задумке: как я уже говорил, в Mono логика работы с хипом совершенно другая. И такое дробление не только не дает положительного эффекта в плане производительности, но в некоторых случаях даже негативно сказывается на работе с памятью. Поэтому сейчас мы смотрим, как бы лучше соптимизировать такие места, чтобы они у нас эффективно работали не только под Windows, но и под другими ОС (Linux или MacOS).

— С чего вообще следует начинать работу с памятью в рамках повышения производительности приложения?

Андрей Акиньшин: Первое, с чего всегда нужно начинать, — это с замеров. Я видел очень многих программистов, которые пытаются на взгляд что-то понять («наверняка у нас здесь выделяется слишком много объектов — давайте прямо сейчас начнем вот это место оптимизировать»). Но этот подход, особенно в большом приложении, редко заканчивается чем-то хорошим. Конечно же, нужно выполнять замеры.
Есть различные профайлеры памяти для .NET. Например — dotMemory [4]. Это очень хороший инструмент, он позволяет искать утечки памяти, выявлять различные проблемы, смотреть, какие объекты сколько памяти занимают и как они распределены по кучам, поколениям и т.д.
Под Windows профайлеров много, и все они меряют достаточно честно. Если мы говорим про Linux/Mac и mono, там инструментарий для профилировки по памяти намного более скудный, не всегда удается померить то, что хочется.

— Каких инструментов в плане профилирования вам больше не хватает?

Андрей Акиньшин: В первую очередь — нормального профайлера под Linux и Mac.
Например, mono имеет возможности встроенной профилировки — нужно его для этого со специальными ключиками запустить, но возможности там достаточно скудные, работать сложно, результатам можно верить не всегда. С CoreCLR все инструменты по профилированию также находятся в достаточно сыром состоянии. На эту тему много полезной информации в недавних постах Саши Гольдштейна [5], там рассказывается, как вообще начать с этим работать. Увы, не сказал бы, что есть особое удобство в запуске профайлинг-сессий и анализе результатов. Поэтому лично я жду, когда наконец-то появится удобный кроссплатформенный инструмент для профилирования (памяти в том числе).

— Кстати, об эволюции. По мере эволюции .NET и сопутствующего инструментария упрощается ли работа с памятью? Или головной боли становится только больше из-за появления новых механизмов?

Андрей Акиньшин: Я бы сказал, что жизнь постепенно становится лучше. Если мы говорим про Windows, то на полном фреймворке каких-то значимых изменений не было уже достаточно давно, все привыкли к тому, как работает сборщик мусора. Те, кому нужно, знают внутренности и как с ним правильно обращаться, чтобы всем было хорошо. Сейчас набирает популярность кроссплатформенная разработка — под Linux и Mac. И там, к сожалению, с инструментарием не так хорошо. Но постепенно он тоже развивается.

Более подробно об оптимизации приложений .NET, в особенности — о работе с памятью и другими компонентами, Андрей Акиньшин расскажет в рамках своего доклада на DotNext 2017 "Поговорим о памяти [3]".

Автор: JUG.ru Group

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/net/251339

Ссылки в тексте:

[1] Rider: https://www.jetbrains.com/rider/

[2] BenchmarkDotNet: https://github.com/dotnet/BenchmarkDotNet

[3] DotNext: https://dotnext-piter.ru/talks/lets-talk-about-memory/

[4] dotMemory: https://www.jetbrains.com/dotmemory/

[5] Саши Гольдштейна: http://blogs.microsoft.co.il/sasha/

[6] Источник: https://habrahabr.ru/post/325178/