Компания
403,62
рейтинг
31 июля 2015 в 13:00

Разработка → Пять способов оптимизации кода для 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. Но быстродействие каждого конкретного приложения очень сильно зависит не только от среды исполнения, но и от разработчика. Надеемся, наши советы по оптимизации кода помогут вам в написании быстрых и удобных приложений.
Автор: @CooperMaster Anil Kumar
Intel
рейтинг 403,62

Комментарии (15)

  • +2
    А разве proguard не делает тоже самое?
    • 0
      Даже если он делает всё вышеперечисленное, использовать локальные переменные вместо полей — хорошая практика (если что, в поиске таких мест может помочь IDEA).
      Опять же помечать поля как final, если они неизменяемые, тоже хорошая идея. Например, у меня в IDEA по умолчанию и в результате Bind constructor arguments to fields, и в Extract variable, и наверняка где-то ещё, стоит галочка «Make final». Теоретически и компилятор Java, и виртуальные машины, и мало ли кто ещё — могут оптимизировать такие вещи. Но зачем надеяться на них, если сама среда разработки может делать код таким :). Каши не просит, времени не отнимает.
      • +1
        Не везде это может оказаться полезным, как минимум в методе onDraw так делать не нужно.
        • 0
          Не совсем понял, где может не оказаться полезным, когда программист метит неизменяемые поля как final?
          Или то, что сообщает, что метод не может быть переопределён в наследнике и надо вызывать именно метод из класса A?
          Я не программист на Android, но насколько я понимаю, onDraw — это метод, который выполняется ну просто очень-очень-очень много раз, нет? Соответственно, все эти вещи наоборот, должны помочь ускорить выполнение, но не замедлить его.

          Правда,
          class A {
            final int v = 42;
          }

          думаю, было бы лучше поменять на
          class A {
            public static final int V = 42;
          }

          Ибо кто его знает, сколько экземпляров класса A создадут.
          • +2
            Я писал в контексте
            использовать локальные переменные вместо полей
            Метод onDraw вызывается каждый кадр, потому будет нагрузка на GC.
            • +1
              Понятно, спасибо. Да, так понятно. Мы в своё время на Java архиватор портировали с C и наступали на эти грабли.
            • 0
              А причём тут GC? Локальные-то переменные отношения к GC не имеют.
  • +2
    Извините, но кто же так abs находит:
    public static final int abs(int a) {
        int b;
        if (a < 0) 
          b = a;
        else
          b = -a;
        return b;
    }
    

    Зачем лишнюю переменную создавать? Почему бы не так:
    public static final int abs(int a) {
        return a < 0 ? -a : a;
    }
    
    • 0
      Потому автор и написал, что не нужно изобретать велосипед, а использовать по возможности стандартные возможности.
    • 0
      как у вас получилось улучшить код тернарным оператором, но не заметить ошибку в логике? хотя в своем примере, вы этой ошбики не допустили :)
      • 0
        В том-то и суть. Рад, что еще кто-то заметил.
  • 0
    Под android, к сожалению, нет такой мощного инструмента для написания тестов на производительность как есть для hotspot — JMH openjdk.java.net/projects/code-tools/jmh
  • 0
    Интересная подача диаграмм!!! Один раз красный столбик это хорошо, другой раз красный столбик плохо… Путаница какая-то.
    • +3
      Красный столбик везде обозначает Android 5, какие проблемы?
  • +4
    скиншот с пустой неэффективной областью на 80% не очень уместен в статье про оптимизацию

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

Самое читаемое Разработка