Советы по производительности

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

Есть два основных правила написания эффективного кода:

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

Одной из самых сложных проблем, с которой вы будете сталкиваться для микро-оптимизации в Android приложений, это то, что ваше приложение будет, несомненно, работает на нескольких типах оборудования. Различные версии виртуальной машин, работающие на разных процессорах, работающих на различных скоростях. Это даже не тот случай, когда вы можете просто сказать "устройство X с коэффициентом F быстрее/медленнее, чем устройство Y", и масштабировать свои результаты с одного устройства на другое. В частности, измерение на эмуляторе говорит вам очень мало о производительности на каком-либо устройстве. Есть также огромные различия между устройствами с и без JIT: лучший код устройства с JIT не всегда лучший код для устройства без него.

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

Избегайте создания ненужных объектов

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

Как только вы выделите несколько объектов в вашем приложении, вы запустите периодическую сборку мусора, создавая небольшую "икоту" пользовательского опыта. Параллельный сборщик мусора представленный в Android 2.3 помогает, но всегда следует избегать ненужной работы.

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

  • Если у вас есть метод, возвращающий строку, и вы знаете, что его результат будет всегда добавляется к StringBuffer в любом случае, измените вашу сигнатуру и реализацию так, чтобы функция выполняла добавление непосредственно, вместо того чтобы создавать кратковременный объект.
  • При извлечении строк из набора входных данных, попытайтесь вернуть подстроку из исходных данных, вместо того чтобы создавать копию. Вы будете создавать новый String объект, но он будет использовать общий char[] с данными. (Компромисс в том, что если вы используете только небольшую часть исходных входных данных, вы будете все равно держать всю память вокруг, если вы идете по этому пути.)

Несколько более радикальная идея это нарезать многомерные массивы в параллельные отдельные одномерные массивы:

  • Массив int гораздо лучше, чем массив Integer объектов, но это также обобщается с тем, что два параллельных массива целых чисел также являются на много более эффективным, чем массив (int,int) объектов. То же самое касается любых комбинаций примитивных типов.
  • Если вам нужно реализовать контейнер, в котором хранятся кортежи (Foo,Bar) объектов, попробуйте вспомнить, что два параллельных Foo[] и Bar[] массива, как правило, гораздо лучше, чем один массив (Foo,Bar) объектов. (Исключением из этого правила, конечно, это когда вы проектируете API для доступа из другого кода. В этих случаях, обычно лучше сделать небольшой компромисс в скорости, чтобы достичь хорошего дизайна API. Но в своем собственном внутреннем коде, вы должны попробовать и быть как можно более эффективным.)

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

Статические методы предпочтительнее виртуальных

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

Используйте static final для констант

Рассмотрим следующее объявление в верхней части класса:

static int intVal = 42;
static String strVal = "Hello, world!";

Компилятор генерирует метод инициализации класса, называемый <clinit>, , который выполняется, когда класс впервые используется. Метод сохраняет значение 42 в intVal, и извлекает ссылку из таблицы констант файла класса для strVal. Когда к этим значениям обращаются позже, они доступны при помощи поиска полей.

Мы можем улучшить ситуацию с помощью ключевого слова "final":

static final int intVal = 42;
static final String strVal = "Hello, world!";

Класс больше не требует <clinit> метод, потому что константы задаются в статических инициализаторах полей в DEX файле. Код, который ссылается на intVal будет использовать целое значение 42 непосредственно, и обращение к strVal будет использовать относительно недорогую инструкцию "строковой константы" вместо поиска поля.

Примечание: Эта оптимизация относится только к примитивным типам и String константам, не произвольным ссылочным типам. Тем не менее, это хорошая практика объявлять константы с помощью static final где это только возможно.

Избегайте внутренних Getters/Setters

В машинных языках таких как C++, обычной практикой является использования геттеров(i = getCount()) вместо доступа непосредственно к полю(i = mCount). Это отличная привычка для C++ и часто практикуется в других объектно-ориентированных языках, таких как C# и Java, так как компилятор может встраивать доступ, и если вам нужно ограничить или проверить доступ к полю, код можно добавить в любое время.

Однако, это плохая идея для Android. Виртуальные вызовы методов очень дорогие, гораздо больше чем поиск полей экземпляров. Разумно следовать общим объектно-ориентированные методам программирования и иметь методы для получения и установки в общественном интерфейсе, но в пределах класса вы всегда должны обращаться к полям напрямую.

Без JIT, прямой доступ к полю примерно в 3 раза быстрее, чем вызов тривиального геттера. С JIT (где прямой доступ к полю такой же дешевы, как доступ к локальным переменным), прямой доступ к полям около 7 раз быстрее, чем вызов тривиальное геттера.

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

Используйте улучшенный синтаксис цикла

Улучшенный for цикл (также иногда называют "for-each" циклом) может быть использован для коллекций, которые реализуют Iterable интерфейс и для массивов. С коллекциями, итератор выделяется для выполнения вызова методов интерфейса hasNext() и next(). С ArrayList, рукописный цикл со счетчиком около 3x раз быстрее (с или без JIT), но и для других коллекций улучшенный синтаксис цикла будет эквивалентно явному использованию итератора.

Есть несколько альтернатив для итерации по массиву:

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

zero() является самым медленным, поскольку JIT пока не может оптимизировать затраты на получение длины массива каждый раз для каждой итерации цикла.

one() быстрее. Вытягивает все из в локальных переменных, избегая поиска. Только длина массива представляет выигрыш в производительности.

two() является самым быстрым для устройств без JIT, и ничем не отличаются от one() для устройств с JIT. Он использует улучшенный синтаксиса цикла, введенный в версии 1.5 языка программирования Java.

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

Полезный совет: См. также Josh Bloch Effective Java, пункт 46.

Рассмотрите использование модификатора доступа package вместо private доступа ко внутренним private классам

Рассмотрим следующее определение класса:

public class Foo {
    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }

    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 27;
        in.stuff();
    }

    private void doStuff(int value) {
        System.out.println("Value is " + value);
    }
}

Что важно здесь, так это то, что мы определяем приватный внутренний класс(Foo$Inner) который напрямую обращается к приватному методу и приватному полю экземпляра внешнего класса. Это является законным, и код выводит "Value is 27", как и ожидалось.

Проблема в том, что VM считает прямой доступ к Foo private членам из Foo$Inner незаконным, поскольку Foo и Foo$Inner различные классы, хотя язык Java разрешает внутренним классам иметь доступ к закрытым членам внешнего класса. Для преодоления разрыва, компилятор генерирует пару искусственных методов:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

Код внутреннего класса вызывает эти статические методы, когда ему нужно получить доступ к mValue полю или вызывать doStuff() метод внешнего класса. Что это означает, что приведенный выше код на самом деле сводится к случаю, когда вы обращаетесь к полям через методы доступа. Ранее мы говорили о том, как средства доступа работают медленнее, чем прямой доступ к полям, так что это является примером определенной идиомы языка, в результате которой получается "невидимое" снижение производительности.

Если вы используете такой код связанный с производительностью, вы можете избежать накладных расходов, объявив поля и методы, к которым обращается внутренний класс с помощью package доступа вместо private. К сожалению, это означает, что поля будут доступны непосредственно другим классами в том же пакете, так что вы не должны использовать это в общедоступном API.

Избегайте вычислений с плавающей точкой

Как правило, вычисления с плавающей точкой работает примерно в 2 раза медленнее, чем целочисленные вычисления на Android устройствах.

С точки зрения скорости, нет никакой разницы между float и double на более современном оборудовании. С точки зрения размерности, double в 2 раза больше. На настольных компьютерах, предполагается, что пространство не является проблемой, и вы предпочтёте double вместо float.

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

Познавайте и используйте библиотеки

В дополнение ко всем обычным причинам предпочтения кода библиотеки вместо написания своей собственной, имейте в виду, что система может свободно заменить вызовы методов библиотеки с ручным ассемблерным кодом, которые могут быть лучше, чем лучший код, который JIT может создать для эквивалента Java. Типичным примером здесь является String.indexOf() и связанные с ними API, который Dalvik заменяет на внутренний внедряемый код. Аналогичным образом, System.arraycopy() метод в 9 раз быстрее, чем рукописный код цикла на Nexus One с JIT.

Полезный совет: См. также Josh Bloch Effective Java, пункт 47.

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

Разработка вашего приложения с платформо-зависимым кодом с помощью Android NDK не обязательно более эффективный, чем программирование на языке Java. С одной стороны, есть затраты, связанные с переходом между Java и машинным кодом, и JIT не может оптимизировать по этим границам. Если вы выделяете ресурсы в машинном коде (память в куче, файловые дескрипторы, или любые другие), может оказаться значительно труднее организовать своевременный сбор этих ресурсов. Кроме того, необходимо скомпилировать код для каждой архитектуры, на которой вы хотите запустить (а не полагаться на имеющийся JIT). Вы можете даже составить несколько версий того, что вы считаете такой же архитектурой: машинный код скомпилированный для процессоров ARM в G1 не может использовать в полной мере преимуществами ARM в Nexus One, и код скомпилированный для ARM в Nexus One не будет работать на ARM в G1.

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

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

Полезный совет: См. также Josh Bloch Effective Java, пункт 54.

Мифы о производительности

На устройствах без JIT, вызовы методов через переменную с точным типом, а не через интерфейс, немного более эффективны. (Так, например, было бы дешевле вызывать методы на HashMap map чем на Map map, хотя в обоих случаях хэш-таблица была HashMap.) Это не тот случай, когда это было в 2 раз медленнее; фактическая разница была примерно на 6% медленнее. Кроме того, JIT делает это практически неразличимым.

На устройствах без JIT, кэширование поля составляет около 20% быстрее, чем неоднократный доступ к полю. С JIT, стоимость доступа к полю примерно такая же, как доступ к локальным переменным, так что не стоит здесь оптимизировать, если вы не кажется, что это сделает ваш код более удобным для чтения. (Это касается также final, static, и static final полей.)

Всегда измеряйте

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

Каждое утверждение, сделанное в этом документе, подкреплены тестами производительности. Источники этих показателей можно найти в code.google.com "dalvik" project.

Тесты производительности построены с помощью Caliper инструмента микро-тестирования для Java. Микро-тесты трудно сделать правильно, поэтому Caliper старается изо всех сил сделать тяжелую работу за вас, и даже обнаружить некоторые случаи, когда вы не измеряете то, что вы думаете, что измеряете (потому что, скажем, VM удалось оптимизировать весь ваш код). Мы настоятельно рекомендуем вам использовать Caliper для запуска собственных микро-тестов.

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

Чтобы получить дополнительную информацию о профилировании и отладке приложений, см. следующие документы: