Пять способов оптимизации кода для Android 5.0 Lollipop

от автора

Как сделать программы быстрее? Один из эффективных способов – оптимизация кода. Зная особенности платформы, для которой создаётся приложение, можно найти эффективные способы его ускорения.


Предварительные сведения

ART (Android RunTime) – это новая среда исполнения Android-приложений. В Android 5.0 Lollipop ART впервые используется по умолчанию. Она включает в себя множество усовершенствований, направленных на улучшение производительности. В этом материале мы расскажем о некоторых новых возможностях ART, сравним её с ранее используемой средой исполнения Android Dalvik и поделимся пятью советами, которые позволят сделать ваши приложения быстрее.

Что нового в ART?

В ходе профилирования множества Android-приложений, которые исполнялись в среде Dalvik, были обнаружены две ключевые особенности, на которые пользователи обращают особое внимание. Первая особенность – время, необходимое для запуска приложения. Вторая – количество разного рода «замедлений» (jank). В худших проявлениях это – запинающийся звук, дёрганая анимация, а то и вовсе – неожиданная остановка приложения. Обычно случается это из-за того, что приложению требуется слишком много времени для подготовки следующего кадра. Как результат, оно просто не успевает за частотой обновления экрана устройства. Скорость формирования кадров может стать проблемой в том случае, если следующий кадр формируется гораздо быстрее или медленнее, чем предыдущий. Если происходит что-то подобное, пользователь видит рывки в работе элементов интерфейса. Это делает взаимодействие с программой гораздо менее удобным, чем хотелось бы и пользователям, и разработчикам. В ART есть несколько новых возможностей, предназначенных для решения вышеописанных проблем.

  • Компиляция перед исполнением. ART компилирует приложения во время установки, используя средство dex2oat, установленное на устройстве. В результате получается скомпилированный под целевую архитектуру исполняемый файл. Для сравнения, Dalvik использует интерпретатор и компилирует приложения «на лету». Во время установки Dalvik конвертирует APK-файлы в оптимизированный DEX-код, а уже во время запуска приложения компилирует его в машинные инструкции. В результате в ART-среде приложения запускаются быстрее, хотя время, которое нужно на установку, увеличивается. Кроме того, при таком подходе приложения используют больше флэш-памяти устройства, так как для хранения скомпилированного во время установки кода требуется дополнительное место.
  • Улучшенные механизмы выделения памяти. Приложения, которые интенсивно используют память, могут испытывать трудности с производительностью при использовании Dalvik. Смягчить эту проблему помогает отдельное пространство для хранения данных больших объектов и улучшенный механизм выделения памяти в ART.
  • Улучшенная сборка мусора. ART оснащён более быстрым сборщиком мусора, который поддерживает параллельную обработку данных, что приводит к меньшей фрагментации памяти и к более эффективному её использованию.
  • Улучшенная производительность JNI. Оптимизация вызова JNI-кода и возврата из него, благодаря которой уменьшается количество инструкций, необходимых для выполнения JNI-вызовов.
  • Поддержка 64-битных архитектур. ART прекрасно чувствует себя на 64-битных архитектурах. Это улучшает производительность многих приложений при запуске их на соответствующем аппаратном обеспечении.

Совместный эффект от этих усовершенствований улучшает восприятие пользователями как приложений, которые написаны только с использованием Android SDK, так и программ, которые интенсивно используют JNI-вызовы. К дополнительным преимуществам, с точки зрения пользователей, можно отнести большее время работы устройства от одной зарядки. Дело здесь в том, что приложения компилируются лишь один раз, они быстрее запускаются, как результат – потребляют меньше энергии батареи при повседневном использовании.

Сравнение производительности ART и Dalvik

Когда ART только выпустили, в виде предварительной версии на Android KitKat 4.4, появились критические замечания о его производительности в сравнении с Dalvik. Надо сказать, что такое сравнение нельзя назвать честным. Ведь сравнивали раннюю предварительную версию ART со зрелым продуктом, подвергнутым за годы работы над ним множеству улучшений. В результате этих ранних тестов некоторые приложения работали в ART-среде медленнее, чем в Dalvik.

Сейчас у нас появилась возможность сравнить повзрослевшую среду ART, которая используется в массово производимых устройствах, с Dalvik. Так как в Android 5.0 используется только ART, прямое сравнение ART и Dalvik возможно лишь в том случае, если сначала выполнить тесты на некоем устройстве с установленным Android KitKat 4.4, получив данные для Dalvik-среды, затем – обновить его до Android Lollipop 5.0 и провести ту же серию тестов для ART-окружения.

При подготовке этого материала мы проделали подобные тесты с планшетом SurfTab xintron i7.0, который построен на базе процессора Intel Atom. Сначала на нём была установлена Android 4.4.4, и, соответственно, при испытаниях использовался Dalvik, потом устройство обновили до Android 5.0. и протестировали быстродействие ART.

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

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

Исходя из полученных данных, можно сделать вывод о том, что ART превосходит Dalvik во всех из проведенных нами тестов. В некоторых случаях это превосходство весьма значительно.


Относительные показатели тестирования ART (Android Lollipop) и Dalvik (Android KitKat)

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

IcyRocks версии 1.0 – это приложение для тестирования производительности устройств, созданное Intel. Оно имитирует реальные компьютерные игры. Для большинства расчётов в нём используется библиотека с открытым исходным кодом Cocos2D и библиотека JBox2D (физический движок, Java Physics Engine). Приложение измеряет среднее количество кадров, которое ему удаётся вывести в секунду (FPS, Frame Per Second) при различных уровнях нагрузки. Затем вычисляет итоговый показатель, беря среднее геометрическое показателей FPS, полученных в различных режимах работы. Кроме того, программа вычисляет уровень «неправильных» кадров в секунду (jank per second), как среднее между такими кадрами на различных уровнях нагрузки. IcyRocks показывает превосходство ART над Dalvik.


Относительные показатели количества кадров в секунду при тестировании производительности в средах ART и Dalvik

В результате тестирования удалось выяснить, что в ART характеристики кадров более постоянны, чем в Dalvik, с меньшим количеством «неправильных» кадров. Как результат, в ART интерфейс приложений работает более гладко.


Относительные показатели «неправильных» кадров в секунду при тестировании в средах ART и Dalvik

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

Перенос программного кода с Dalvik на ART

Переход от Dalvik к ART прозрачен, большинство приложений, которые работают в среде Dalvik, будут работать и в среде ART без необходимости модификации их кода. В результате, когда пользователи обновляют систему, приложения начинают работать быстрее без каких-либо дополнительных усилий. Однако, особенно если ваши приложения используют Java Native Interface, их не помешает протестировать в среде ART. Дело в том, что в ART используется более строгий механизм обработки JNI-ошибок, чем в Dalvik. Здесь можно узнать подробности об этом.

Пять советов по оптимизации кода

Производительность большинства приложений, которые будут запускаться в среде ART, увеличится только из-за тех улучшений платформы, о которых мы говорили выше. Однако, существует набор рекомендаций, следуя которым можно оптимизировать приложения для достижения еще большей производительности. Каждый из описанных ниже приёмов оптимизации кода снабжён простым примером кода, иллюстрирующим особенности его работы.

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

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

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

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

В блоке неоптимизированного кода, который показан ниже, значение переменной v вычисляется во время исполнения приложения. Это происходит из-за того, что данная переменная доступна за пределами метода m() и может быть изменена в любом участке кода. Поэтому значение переменной неизвестно на этапе компиляции. Компилятор не знает, изменит ли вызов метода some_global_call() значение этой переменной, или нет, так как переменную v, повторимся, может изменить любой код за пределами метода.

В оптимизированном варианте этого примера v – это локальная переменная. А значит, её значение может быть вычислено на этапе компиляции. Как результат – компилятор может поместить значение переменной в код, который он генерирует, что поможет избежать вычисления значения переменной во время выполнения.

Неоптимизированный код Оптимизированный код
class A {   public int v = 0;    public int m(){     v = 42;     some_global_call();      return v*3;    } }
class A {   public int m(){     int v = 42;     some_global_call();     return v*3;    }  }   

Совет №2. Используйте ключевое слово final для того, чтобы подсказать компилятору то, что значение поля – константа

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

Во фрагменте неоптимизированного кода значение v*v*v должно вычисляться во время выполнения программы, так как значение v может измениться. В оптимизированном варианте использование ключевого слова final при объявлении переменной и присвоении ей значения, говорит компилятору о том, что значение переменной меняться не будет. Таким образом, вычисление значения можно произвести на этапе компиляции и в выходной код будет добавлено значение, а не команды для его вычисления во время выполнения программы.

Неоптимизированный код Оптимизированный код
class A {   int v = 42;    public int m(){     return v*v*v;   }  } 
class A {   final int v = 42;    public int m(){     return v*v*v;     }  }

Совет №3. Используйте ключевое слово final при объявлении классов и методов

Так как любой метод в Java может оказаться полиморфными, объявление метода или класса с ключевым словом final указывает компилятору на то, что метод не переопределён ни в одном из подклассов.

В неоптимизированном варианте кода перед вызовом функции m() нужно произвести её разрешение.
В оптимизированном коде, из-за использования при объявлении метода m() ключевого слова final, компилятор знает, какая именно версия метода будет вызвана. Поэтому он может избежать поиска метода и заменить вызов метода m() его содержимым, встроив его в необходимое место программы. В результате получаем увеличение производительности.

Неоптимизированный код Оптимизированный код
class A {   public int m(){     return 42;     }    public int f(){     int sum = 0;      for (int i = 0; i < 1000; i++)       sum += m(); // m необходимо разрешить перед вызовом      return sum;   } }
class A {   public final int m(){     return 42;     }    public int f(){     int sum = 0;      for (int i = 0; i < 1000; i++)       sum += m();       return sum;   }  }

Совет №4. Избегайте вызывать маленькие методы через JNI

Существуют веские причины использования JNI-вызовов. Например, если у вас есть код или библиотеки на C/C++, которые вы хотите повторно использовать в Java-приложениях. Возможно, вы создаёте кросс-платформенное приложение, или ваша цель – увеличение производительности за счет использования низкоуровневых механизмов. Однако, важно свести количество JNI-вызовов к минимуму, так как каждый из них создаёт значительную нагрузку на систему. Когда JNI используют для оптимизации производительности, эта дополнительная нагрузка может свести на нет ожидаемую выгоду. В частности, частые вызовы коротких, не производящих значительной вычислительной работы JNI-методов, способны производительность ухудшить. А если такие вызовы поместить в цикл, то ненужная нагрузка на систему лишь увеличится.

Пример кода

class A {   public final int factorial(int x){     int f = 1;     for (int i =2; i <= x; i++)       f *= i;     return f;     }    public int compute (){     int sum = 0;      for (int i = 0; i < 1000; i++)       sum += factorial(i % 5);  //если мы воспользуемся здесь JNI-вариантом функции factorial(),  // приложение будет работать заметно медленнее,  // так как вызов происходил бы в цикле // а это лишь усиливает нагрузку на систему в ходе JNI-вызовов     return sum;   } }

Совет №5. Используйте стандартные библиотеки вместо реализации той же функциональности в собственном коде

Стандартные библиотеки Java серьёзно оптимизированы. Если использовать везде, где это возможно, внутренние механизмы Java, это позволит достичь наилучшей производительности. Стандартные решения могут работать значительно быстрее, чем «самописные» реализации. Попытка избежать дополнительной нагрузки на систему за счёт отказа от вызова стандартной библиотечной функции может, на самом деле, ухудшить производительность.
В неоптимизированном варианте кода показана попытка избежать вызова стандартной функции Math.abs() за счёт собственной реализации алгоритма получения абсолютного значения числа. Однако, код, в котором вызывается библиотечная функция, работает быстрее за счёт того, что вызов заменяется оптимизированной внутренней реализацией в ART во время компиляции.

Неоптимизированный код Оптимизированный код
class A {   public static final int abs(int a){     int b;     if (a < 0)        b = a;     else       b = -a;     return b;   }  }
class A {   public static final int abs (int a){     return Math.abs(a);    }  }      

Тестирование техник оптимизации

Выясним, какова разница в производительности оптимизированного и неоптимизированного кода из совета №2 при запуске его в среде ART. Для эксперимента будем использовать планшет Asus Fonepad 8, построенный на базе CPU Intel Atom Z3530. Устройство обновлено до Android 5.0.

Вот код, который мы подвергаем испытаниям:

public final class Ops {     int v = 42;     final int w = 42;      public int testUnoptimized(){         return v*v*v;     }      public int testOptimized(){         return w*w*w;     } }

Разница методов testUnoptimized и testOptimized заключается лишь в том, что второй оптимизирован, переменная w, которая в нём используется, объявлена с ключевым словом final.

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


Интерфейс приложения для тестирования результатов оптимизации

В таблице показаны результаты десяти последовательных запусков теста в release-версии приложения. Каждый из отдельных показателей получен в результате выполнения циклического вызова соответствующего метода 10 миллионов раз.

Сравнение скорости выполнения оптимизированного и неоптимизированного кода

Оптимизировано, мс. Не оптимизировано, мс.
1 25 193
2 21 203
3 30 220
4 25 175
5 23 184
6 28 177
7 30 186
8 27 191
9 34 212
10 27 174
Среднее 27 191.5

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

Исходный код проекта, подходящий для импорта в Android Studio можно найти здесь.

Оптимизации Intel в ART

Intel работала с OEM-производителями устройств, предоставляя им оптимизированную, в расчёте на процессоры Intel, версию Dalvik. То же самое происходит и в случае с ART, в результате производительность новой среды исполнения, со временем, будет увеличиваться. Оптимизированные версии кода можно будет получить либо в Android Open Source Project (AOSP), либо – напрямую у производителей устройств. Как и прежде, оптимизации прозрачны и для пользователей и для разработчиков, то есть, для того, чтобы воспользоваться их преимуществами, ни тем ни другим не придётся прилагать дополнительных усилий.

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

Итоги

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

ссылка на оригинал статьи http://habrahabr.ru/post/263873/