Производительность мобильных приложений — краеугольный вопрос, вне зависимости от рассматриваемой платформы. О том, какую специфику эта проблема имеет под Android, в том числе в ракурсе перехода с виртуальной машины Dalvik на ART, мы поговорили с Александром Ефременковым.
– Расскажите пару слов о себе и своей работе. Какую роль в работе играют эксперименты с производительностью под Android?
Александр Ефременков: Я работаю в Яндексе, где занимаюсь разработкой таксометра «Яндекс.Такси».
Специфика нашего проекта отличается от обычных entertainment-решений. Сервис обеспечивает взаимодействие двух сторон: на одном конце «провода» есть пользователь с фронт приложением Такси, а на другом — пользователь таксометра. И в ходе взаимодействия обе стороны должны получить ожидаемый результат.
Проект сложен в первую очередь из-за наличия большого количества арифметики, необходимой для обсчёта тарифов. При этом требования к стабильности очень серьезные: мы просто обязаны предоставить водителям высокопроизводительный и надёжный код. В обычных приложениях, где можно, условно говоря, просматривать котят, подобные требования не особо актуальны.
В свободное время я занимаюсь исследованием внутренних особенностей Android, в частности, того, чем он отличается от окружения родительской JDK.
– С какими «граблями» производительности вы чаще всего сталкиваетесь в работе? Откуда начинается поиск источников проблем?
Александр Ефременков: Детали, конечно, под NDA. Но могу сказать, что большинство проблем производительности обусловлены выбором алгоритмов в уже написанном коде. При этом первая стадия поиска узких мест, как правило, — это поиск проблем в написанном коде, а уже вторая — обход проблем рантайма.
– Есть ли какая-то первопричина проблем, определяющих сложность борьбы за производительность в Android?
Александр Ефременков: Проблема в том, что в большинстве случаев разработчики думают, что рантайм-оптимизации (JIT/AOT) в Android работают так же, как и в JDK. Но это не так.
Существует целый ряд отличий, и нам с этим надо как-то жить. И для этого нужно разбираться во внутренностях рантайма Android. Надо понимать, как получить адекватные метрики производительности, как сравнить собственноручно написанные конструкции, чтобы понять, какую из них выбрать для продакшн.
– А можете ли вы привести какие-нибудь примеры упомянутых отличий?
Александр Ефременков: Хороший пример — оптимизации выброса большого количества идентичных исключений. JDK умеет оптимизировать выброс одинаковых исключений путём отображения только throwable message без построения стектрейса (формирование стектрейса — достаточно тяжелая операция). Dalvik/ART данный трюк не использует и честно кэширует call stack для последующей его распечатки.
Также стоит упомянуть dead code elimination в Android. В большинстве случаев его просто нет, в то время как в JDK эта оптимизация встречается довольно часто.
Ещё одно интересное отличие — AOT компиляция, которая позволяет оптимизировать и сохранять машинный код, а также сбрасывать/разворачивать AOT участки на диск и с диска.
Эти и другие моменты необходимо учитывать в работе по улучшению производительности приложения.
– С чего вообще должна начинаться работа над производительностью приложения под Android?
Александр Ефременков: С измерений. Первый шаг к производительности — flame graphs, а уже после должна идти оптимизация узких мест.
Сама по себе оптимизация узких мест как раз и заключается в том, чтобы в идеальном случае эти места переписать так, чтобы попасть под рантайм-оптимизацию, а не угодить в деоптимизацию. Т.е. после очередных манипуляций с кодом необходимо измерять переписанный прототип под разными условиями.
– Как вообще выглядит ситуация с замерами? Раз производительность как таковая является объектом изысканий, значит «из коробки» среднестатистический разработчик не может «выжать максимум», не разбираясь в деталях?
Александр Ефременков: В среднем ситуация с замерами выглядит следующим образом: сначала разработчик прибегает к трассировке и поиску узких мест по готовому flame graph. Получив какие-то результаты, он переписывает свой код в тех местах, где был выявлен долгий cpu time. При этом итоговый вариант кода зачастую выбирается опытным путем с нескольких попыток (какой вариант лучше себя показывает в упомянутых замерах — тот и используется).
Сложность этой ситуации — в отсутствии «системного» подхода. Обычно решения проблем подобным образом валидируются малым количеством крайних случаев. А это поднимает вопрос о соответствии проведенных замеров изначально поставленной задаче.
– Получается, встроенные в Android SDK средства измерений не дают нужной информации?
Александр Ефременков: К сожалению, средства Android SDK дают достаточное количество информации только об исполняемом коде, а не о том, какие оптимизации применяются внутри рантайма Android. К примеру, нельзя получить информацию о том, почему ART оптимизируется при вызове getter/setter и убирает ненужные проверки на null, и почему Dalvik эти оптимизации делает не всегда. Обычными инструментами подобные реализации поведения и оптимизации просто невозможно увидеть — только если ты сам разработчик рантайма и обладаешь соответствующими знаниями.
– Каких инструментов больше всего не хватает в Android SDK?
Александр Ефременков: Инструментарий Android на данный момент претерпевает множество изменений из-за прогрессивной эволюции платформы — перехода с Dalvik на ART.
На данный момент нет адекватного инструмента для создания бенчмарков. Эта проблема и определяет описанный мной ранее подход к замерам. Разработчик вынужден писать так, как ему подсказывает сердце. А провести адекватный эксперимент он не может из-за недостатка инструментария. Единственное, что ему остается — обернуть вызов метода в try finally, который измеряет этот вызов двумя метками времени: начальной и конечной. Но, к сожалению, на результаты экспериментов подобного рода нельзя полностью положиться.
– В чем именно проблема подобных экспериментов?
Александр Ефременков: Проблема в замерах через try finally (т.е. с двумя тайм-стемпами) описывается двумя тезисами:
Зачастую проблема решается законом больших чисел: вызов прогоняется n раз и общее время вызова делится на n.
– А сторонними инструментами эту проблему решить можно?
Александр Ефременков: На данный момент решить проблему точных замеров сторонними инструментами сложно, т.к. сейчас эти инструменты работают под JDK (к примеру, JMH).
Но ситуация постепенно меняется. К слову говоря, уже близится к завершению моя собственная разработка — его порт, AMH — Android Microbenchmark Harness, который учитывает специфику Android Dalvik/ART.
– Одинакова ли ситуация с оптимизацией в Dalvik/ART? Или с переходом на ART жить, условно говоря, стало лучше?
Александр Ефременков: Как я упоминал выше, Dalvik и ART принципиально отличаются: они используют разные оптимизации и, соответственно, дают совершенно разные конечные результаты.
Безусловно, с переходом на ART стало жить получше. По крайней мере регрессивные дырки некоторым приложениям это закрыло. Однако ART решает проблему первичного AOT компилирования, но последующие проблемы компиляции на лету он не решает. Так или иначе рантайму нужна информация и некоторый «прогрев», чтобы понять какую AOT/JIT оптимизацию исполнить, и можно ли её исполнить вообще.
– Что вы посоветуете нашим читателям вместо заключения в рамках набора опыта в области производительности под Android?
Александр Ефременков: Совет могу дать только один: надо писать код и пытаться понимать, как он работает. И в любой непонятной ситуации необходимо обращаться к исходному коду платформы, чтобы понять, что именно происходит внутри. Иначе есть риск построить в своей голове воздушные замки с недостоверной информацией, которые могут в итоге разрушиться из-за незнания особенностей платформы (тем более, что эти особенности отличаются из-за фрагментации платформ).
Если вас также интересуют другие аспекты разработки приложений под Android и iOS — спешим пригласить на нашу конференцию для разработчиков Mobius 2017, которая стартует в эту пятницу 21.04.
– Расскажите пару слов о себе и своей работе. Какую роль в работе играют эксперименты с производительностью под Android?
Александр Ефременков: Я работаю в Яндексе, где занимаюсь разработкой таксометра «Яндекс.Такси».
Специфика нашего проекта отличается от обычных entertainment-решений. Сервис обеспечивает взаимодействие двух сторон: на одном конце «провода» есть пользователь с фронт приложением Такси, а на другом — пользователь таксометра. И в ходе взаимодействия обе стороны должны получить ожидаемый результат.
Проект сложен в первую очередь из-за наличия большого количества арифметики, необходимой для обсчёта тарифов. При этом требования к стабильности очень серьезные: мы просто обязаны предоставить водителям высокопроизводительный и надёжный код. В обычных приложениях, где можно, условно говоря, просматривать котят, подобные требования не особо актуальны.
В свободное время я занимаюсь исследованием внутренних особенностей Android, в частности, того, чем он отличается от окружения родительской JDK.
– С какими «граблями» производительности вы чаще всего сталкиваетесь в работе? Откуда начинается поиск источников проблем?
Александр Ефременков: Детали, конечно, под NDA. Но могу сказать, что большинство проблем производительности обусловлены выбором алгоритмов в уже написанном коде. При этом первая стадия поиска узких мест, как правило, — это поиск проблем в написанном коде, а уже вторая — обход проблем рантайма.
– Есть ли какая-то первопричина проблем, определяющих сложность борьбы за производительность в Android?
Александр Ефременков: Проблема в том, что в большинстве случаев разработчики думают, что рантайм-оптимизации (JIT/AOT) в Android работают так же, как и в JDK. Но это не так.
Существует целый ряд отличий, и нам с этим надо как-то жить. И для этого нужно разбираться во внутренностях рантайма Android. Надо понимать, как получить адекватные метрики производительности, как сравнить собственноручно написанные конструкции, чтобы понять, какую из них выбрать для продакшн.
– А можете ли вы привести какие-нибудь примеры упомянутых отличий?
Александр Ефременков: Хороший пример — оптимизации выброса большого количества идентичных исключений. JDK умеет оптимизировать выброс одинаковых исключений путём отображения только throwable message без построения стектрейса (формирование стектрейса — достаточно тяжелая операция). Dalvik/ART данный трюк не использует и честно кэширует call stack для последующей его распечатки.
Также стоит упомянуть dead code elimination в Android. В большинстве случаев его просто нет, в то время как в JDK эта оптимизация встречается довольно часто.
Ещё одно интересное отличие — AOT компиляция, которая позволяет оптимизировать и сохранять машинный код, а также сбрасывать/разворачивать AOT участки на диск и с диска.
Эти и другие моменты необходимо учитывать в работе по улучшению производительности приложения.
– С чего вообще должна начинаться работа над производительностью приложения под Android?
Александр Ефременков: С измерений. Первый шаг к производительности — flame graphs, а уже после должна идти оптимизация узких мест.
Сама по себе оптимизация узких мест как раз и заключается в том, чтобы в идеальном случае эти места переписать так, чтобы попасть под рантайм-оптимизацию, а не угодить в деоптимизацию. Т.е. после очередных манипуляций с кодом необходимо измерять переписанный прототип под разными условиями.
– Как вообще выглядит ситуация с замерами? Раз производительность как таковая является объектом изысканий, значит «из коробки» среднестатистический разработчик не может «выжать максимум», не разбираясь в деталях?
Александр Ефременков: В среднем ситуация с замерами выглядит следующим образом: сначала разработчик прибегает к трассировке и поиску узких мест по готовому flame graph. Получив какие-то результаты, он переписывает свой код в тех местах, где был выявлен долгий cpu time. При этом итоговый вариант кода зачастую выбирается опытным путем с нескольких попыток (какой вариант лучше себя показывает в упомянутых замерах — тот и используется).
Сложность этой ситуации — в отсутствии «системного» подхода. Обычно решения проблем подобным образом валидируются малым количеством крайних случаев. А это поднимает вопрос о соответствии проведенных замеров изначально поставленной задаче.
– Получается, встроенные в Android SDK средства измерений не дают нужной информации?
Александр Ефременков: К сожалению, средства Android SDK дают достаточное количество информации только об исполняемом коде, а не о том, какие оптимизации применяются внутри рантайма Android. К примеру, нельзя получить информацию о том, почему ART оптимизируется при вызове getter/setter и убирает ненужные проверки на null, и почему Dalvik эти оптимизации делает не всегда. Обычными инструментами подобные реализации поведения и оптимизации просто невозможно увидеть — только если ты сам разработчик рантайма и обладаешь соответствующими знаниями.
– Каких инструментов больше всего не хватает в Android SDK?
Александр Ефременков: Инструментарий Android на данный момент претерпевает множество изменений из-за прогрессивной эволюции платформы — перехода с Dalvik на ART.
На данный момент нет адекватного инструмента для создания бенчмарков. Эта проблема и определяет описанный мной ранее подход к замерам. Разработчик вынужден писать так, как ему подсказывает сердце. А провести адекватный эксперимент он не может из-за недостатка инструментария. Единственное, что ему остается — обернуть вызов метода в try finally, который измеряет этот вызов двумя метками времени: начальной и конечной. Но, к сожалению, на результаты экспериментов подобного рода нельзя полностью положиться.
– В чем именно проблема подобных экспериментов?
Александр Ефременков: Проблема в замерах через try finally (т.е. с двумя тайм-стемпами) описывается двумя тезисами:
- Вызов nanoTime — это не операция в вакууме. Она стоит времени и может давать искажения в замерах.
- Гранулярность двух последовательных вызовов nanoTime может не позволить измерить участки, которые не выходят за отрезок времени между двумя этими самыми вызовами nanoTime.
Зачастую проблема решается законом больших чисел: вызов прогоняется n раз и общее время вызова делится на n.
– А сторонними инструментами эту проблему решить можно?
Александр Ефременков: На данный момент решить проблему точных замеров сторонними инструментами сложно, т.к. сейчас эти инструменты работают под JDK (к примеру, JMH).
Но ситуация постепенно меняется. К слову говоря, уже близится к завершению моя собственная разработка — его порт, AMH — Android Microbenchmark Harness, который учитывает специфику Android Dalvik/ART.
– Одинакова ли ситуация с оптимизацией в Dalvik/ART? Или с переходом на ART жить, условно говоря, стало лучше?
Александр Ефременков: Как я упоминал выше, Dalvik и ART принципиально отличаются: они используют разные оптимизации и, соответственно, дают совершенно разные конечные результаты.
Безусловно, с переходом на ART стало жить получше. По крайней мере регрессивные дырки некоторым приложениям это закрыло. Однако ART решает проблему первичного AOT компилирования, но последующие проблемы компиляции на лету он не решает. Так или иначе рантайму нужна информация и некоторый «прогрев», чтобы понять какую AOT/JIT оптимизацию исполнить, и можно ли её исполнить вообще.
– Что вы посоветуете нашим читателям вместо заключения в рамках набора опыта в области производительности под Android?
Александр Ефременков: Совет могу дать только один: надо писать код и пытаться понимать, как он работает. И в любой непонятной ситуации необходимо обращаться к исходному коду платформы, чтобы понять, что именно происходит внутри. Иначе есть риск построить в своей голове воздушные замки с недостоверной информацией, которые могут в итоге разрушиться из-за незнания особенностей платформы (тем более, что эти особенности отличаются из-за фрагментации платформ).
Если вас также интересуют другие аспекты разработки приложений под Android и iOS — спешим пригласить на нашу конференцию для разработчиков Mobius 2017, которая стартует в эту пятницу 21.04.