Разрушение Мифов О Производительности Android



Узнайте, какие мифы о производительности Android выдержали проверку эталонными тестами

В ожидании начала курса «Андроид-разработчик.

Базовый» Приглашаем всех посмотреть открытый урок по теме «Юнит-тестирование в Android».

Также делимся переводами полезного материала.



Разрушение мифов о производительности Android




За прошедшие годы возникло множество мифов о производительности Android. Хотя некоторые мифы могут показаться забавными или забавными, ошибки при создании эффективных приложений для Android — это полная противоположность веселью.

В этой статье мы собираемся проверить эти мифы в духе «Разрушителей мифов».

Для развенчания мифов мы используем реальные примеры и инструменты, которые вы тоже можете использовать.

Мы фокусируемся на преобладающих моделях использования: что вы, как разработчики, скорее всего, будете делать в своем приложении.

Но стоит высказать предостережение: помните, что очень важно сначала провести измерения, прежде чем принимать решение об использовании того или иного метода из соображений производительности.

Тем не менее, давайте приступим к разрушению мифа!

Миф 1: приложения Kotlin больше и медленнее, чем приложения Java

Команда Google Drive перенесла свое приложение с Java на Kotlin. Эта миграция затронула более 16 000 строк кода и 170 файлов, охватывающих более 40 целей сборки.

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



Разрушение мифов о производительности Android

Как видите, переход на Kotlin не оказал существенного влияния.

Более того, команда не заметила разницы в производительности во всем наборе тестов.

Они отметили небольшое увеличение времени компиляции и размера скомпилированного кода, но примерно на 2%, что не имеет большого значения.

С другой стороны, команда добилась сокращения количества строк кода на 25%.

Их код стал чище, понятнее и проще в обслуживании.

В отношении Kotlin следует отметить одну вещь: вы можете и должны использовать инструменты сжатия кода, такие как R8, в котором даже есть специальные оптимизации для Kotlin.

Миф 2: геттеры и сеттеры стоят дорого

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

Общий шаблон кода, в котором getFoo выступает в качестве метода получения, выглядит примерно так:

  
  
  
   

public class ToyClass { public int foo; public int getFoo() { return foo; } } ToyClass tc = new ToyClass();

Мы сравнили это с использованием общедоступного поля tc.foo, где код нарушает инкапсуляцию объекта и обеспечивает прямой доступ к полям.

Мы проверили это с Библиотеки Jetpack Benchmark на Pixel 3 с Android 10. Библиотека тестов предоставляет отличный способ легко протестировать ваш код. Среди особенностей библиотеки — то, что она предварительно разгоняет код, поэтому результаты стабильны.

Что же показали тесты?

Разрушение мифов о производительности Android

Версия с геттером работает так же хорошо, как и версия с прямым доступом к полям.

Этот результат неудивителен, поскольку среда выполнения Android (ART) встраивает в ваш код все тривиальные методы доступа.

Таким образом, код, выполняемый после компиляции JIT или AOT, один и тот же.

Более того, когда вы получаете доступ к полю в Kotlin (в данном примере tc.foo), вы получаете доступ к этому значению с помощью метода получения или установки в зависимости от контекста.

Однако, поскольку мы инлайним все методы доступа, ART нас выручит: разницы в производительности нет. Если вы не используете Kotlin и у вас нет веской причины сделать поля общедоступными, вам не следует нарушать хорошие практики инкапсуляции.

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

Используйте геттеры и сеттеры.



Миф 3: Лямбда-выражения медленнее, чем внутренние классы

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

Сначала используем API потоковой передачи с операцией уменьшения карты.



ArrayList<ToyClass> array = build(); int sum = array.stream().

map(tc -> tc.foo).

reduce(0, (a, b) -> a + b);

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

Это можно сравнить с определением эквивалентных классов для лямбда-выражений.



ToyClassToInteger toyClassToInteger = new ToyClassToInteger(); SumOp sumOp = new SumOp(); int sum = array.stream().

map(toyClassToInteger).

reduce(0, sumOp);

Существует два вложенных класса: один — toyClassToInteger, который преобразует объекты в целое число, а второй — оператор суммы.

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

Однако как насчет различий в производительности? Мы снова использовали библиотеку Jetpack Benchmark на Pixel 3 под управлением Android 10 и не обнаружили никакой разницы в производительности.



Разрушение мифов о производительности Android

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

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

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



Миф 4: Распределение объектов обходится дорого, лучше использовать пулы

Android использует современные функции распределения памяти и сбора мусора.

Распределение объектов улучшалось почти в каждом выпуске, как вы можете видеть на следующем графике.



Разрушение мифов о производительности Android

Сбор мусора также значительно улучшился от выпуска к выпуску.

Сегодня сбор мусора не влияет на сбои или плавность работы приложения.

На следующем графике показано улучшение, которое мы внесли в Android 10 для сбора объектов с коротким сроком жизни с помощью параллельного Gen-CC. Улучшения, которые также заметны в новой версии Android 11.

Разрушение мифов о производительности Android

Пропускная способность значительно увеличилась в тестах по сбору мусора, таких как H2, более чем на 170 %, а в реальных приложениях, таких как Google Sheets, — на 68 %.

Так как же это влияет на решения по написанию, например, следует ли распределять объекты с помощью пулов? Если вы предполагаете, что сбор мусора неэффективен, а выделение памяти обходится дорого, вы можете заключить, что чем меньше мусора вы создаете, тем меньше сбор мусора будет работать.

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

Вы можете реализовать что-то вроде этого:

Pool<A> pool[] = new Pool<>[50]; void foo() { A a = pool.acquire(); … pool.release(a); }

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

Чтобы проверить это, мы реализовали микротест для измерения двух вещей: накладных расходов на стандартное распределение и получение объекта из пула, а также накладных расходов ЦП, чтобы увидеть, влияет ли сбор мусора на производительность приложения.

В этом сценарии мы использовали Pixel 2 XL с Android 10, запуская код распределения тысячи раз в очень узком цикле.

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

Вот результаты накладных расходов на выделение объектов:

Разрушение мифов о производительности Android

Вот результаты нагрузки ЦП на сбор мусора:

Разрушение мифов о производительности Android

Вы можете заметить, что разница между стандартным распределением и использованием объектов из пула минимальна.

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

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

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

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

Итак, развеян ли этот миф? Не совсем.

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

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

  • Может иметь больший объем памяти.

  • Риск сохранения объектов живыми дольше, чем необходимо.

  • Требуется очень эффективная реализация пула.

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

Главное, о чем следует помнить – проверить и измерить, прежде чем выбрать свой вариант.

Миф 5. Профилирование отлаживаемого приложения — хорошая идея

Профилирование вашего приложения во время его отладки было бы очень удобно, поскольку, в конце концов, вы обычно пишете код в режиме отладки.

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

К сожалению нет. Чтобы проверить этот миф, мы рассмотрели несколько тестов для распространенных рабочих процессов.

Результаты представлены на следующем графике.



Разрушение мифов о производительности Android

Некоторые тесты, такие как десериализация, этого не отражают. Однако у других наблюдается регресс эталонного показателя на 50% и более.

Мы даже нашли примеры, которые были на 100% медленнее.

Это связано с тем, что среда выполнения очень мало оптимизирует ваш код во время его отладки, т. е.

код, который пользователи запускают на производственных устройствах, сильно отличается.

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



Очень странные дела

Теперь мы собираемся отойти от развенчания мифов и обратить внимание на более странные вещи.

Это не мифы, которые мы могли бы опровергнуть.

Скорее, это вещи, которые не сразу очевидны или легко анализируются, но результаты могут перевернуть ваш мир с ног на голову.



Странность 1: Multidex: влияет ли это на производительность моего приложения?

APK-файлы становятся все больше и больше.

Они уже некоторое время не вписываются в ограничения традиционной спецификации dex. Multidex — это решение, которое вы должны использовать, если ваш код превышает ограничение метода.

Вопрос в том, сколько методов слишком много? И будет ли влияние на производительность, если в приложении будет большое количество dex-файлов? Возможно, дело даже не в том, что ваше приложение слишком велико: вы можете просто разделить файлы dex на основе функций, чтобы упростить разработку.

Чтобы изучить влияние нескольких файлов dex на производительность, мы воспользовались калькулятором.

По умолчанию это приложение с одним dex-файлом.

Затем мы разделили его на пять файлов dex в зависимости от границ пакета, чтобы имитировать разделение по функциям.

Затем мы протестировали несколько аспектов производительности, начиная с момента запуска.



Разрушение мифов о производительности Android

Разделение dex-файла на это не повлияло.

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

Однако, если вы разумно разделите файл dex и не добавите сотни из них, влияние на время запуска должно быть минимальным.

А как насчет размера APK и памяти?

Разрушение мифов о производительности Android



Разрушение мифов о производительности Android

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

Это связано с тем, что когда вы разделяете приложение на несколько файлов dex, каждый файл dex содержит некоторые повторяющиеся данные для таблиц символов и кэшей.

Однако вы можете минимизировать это увеличение, уменьшив зависимости между файлами dex. В нашем случае мы не пытались его минимизировать.

Если бы мы пытались минимизировать зависимости, мы бы обратились к инструментам R8 и D8. Эти инструменты автоматизируют разделение файлов dex, помогают избежать распространенных ошибок и минимизировать зависимости.

Например, эти инструменты не будут создавать больше dex-файлов, чем необходимо, и не будут помещать все классы запуска в основной файл.

Однако, если вы самостоятельно разделяете файлы dex, всегда измеряйте то, что вы разделяете.



Странность 2: Мертвый код

Одним из преимуществ использования среды выполнения с JIT-компилятором, таким как ART, является то, что среда выполнения может профилировать код, а затем оптимизировать его.

Существует теория, согласно которой, если код не профилируется интерпретатором/JIT-системой, он, вероятно, также не выполняется.

Чтобы проверить эту теорию, мы изучили профили ART, созданные приложением Google. Мы обнаружили, что значительная часть кода приложения не профилируется ART-интерпретатором JIT-системы.

Это признак того, что большая часть кода никогда не будет работать на устройствах.

Существует несколько типов кода, которые невозможно профилировать:

  • Код обработки ошибок, который, надеюсь, выполняется нечасто.

  • Код обратной совместимости — код, который работает не на всех устройствах, особенно на устройствах под управлением Android 5 или более поздней версии.

  • Код для редко используемых функций.

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

Быстрый, простой и бесплатный способ удалить ненужный код — минимизировать его с помощью R8. Затем, если вы еще этого не сделали, преобразуйте свое приложение для использования Пакет приложений для Android и предоставление функций Play. Они позволяют улучшить пользовательский опыт, устанавливая только те функции, которые используются.



выводы

Мы развенчали ряд мифов о производительности Android, но также увидели, что в некоторых случаях все не так однозначно.

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

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

Например, в Android Studio есть профилировщики для собственного и неродного кода, а также профилировщики для использования батареи и сети.

Есть инструменты, которые могут копать глубже, например Perfetto и Systrace. Эти инструменты могут предоставить очень подробное представление о том, что происходит, например, во время запуска приложения или сегмента его выполнения.

Библиотека Jetpack Benchmark избавляет от хлопот, связанных с измерениями и тестированием.

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

И последнее, но не менее важное: не выполняйте профилирование в режиме отладки.

Java является зарегистрированной торговой маркой Oracle и/или ее дочерних компаний.




Узнайте больше о курсе «Андроид-разработчик.

Базовый».

Посмотреть открытый урок по теме «Юнит-тестирование в Android» .

Теги: #разработка для Android #программирование #разработка для Android #веб-разработка #рекомендуемые
Вместе с данным постом часто просматривают: