Читайте также:
|
|
Додаток для платформи Android буде запущено на мобільному пристрої з обмеженими обчислювальними можливостями і пам'яттю, і з недовгим часом роботи батареї. Це значить, програма має бути ефективною. Час роботи батареї - одна з причин, по якій необхідно оптимізувати додаток, навіть якщо він працює досить швидко. Час роботи батареї є дуже важливим для користувачів.
Використання правильних структур даних і алгоритмів дуже сильно покращує продуктивність. Ретельне обмірковування впливу API на продуктивність полегшить перехід на більш якісну реалізацію в подальшому (що в першу чергу важливо для коду бібліотек, ніж коду додатків).
Головною складністю при побудові мобільних додатків, є те, що додаток має з високою ймовірністю працювати на безлічі різних апаратних платформ. Різні версії віртуальної машини на різних процесорах працюють з різною швидкістю. Зокрема, тестування на емуляторі практично нічого не говорить про продуктивність на будь-якому пристрої. Існує також велика різниця між пристроями з і без JIT: «краще» код для пристрою з JIT не завжди залишається кращому для пристрою, його позбавленого.
Створення об’єктів
Створення об'єктів ніколи не буває безкоштовним. Збирач сміття, що працює з поколіннями і пулами для тимчасових об'єктів кожного потоку, може зробити виділення пам'яті простіше, але виділяти пам'ять завжди дорожче, ніж не виділяти її.
Якщо виділяти об'єкти в циклі в інтерфейсі, примусово викликається періодична збірка сміття, створюючи маленькі «заїкання», які видно користувачеві.
Таким чином, слід уникати створення об'єктів, в яких немає необхідності. Пара прикладів:
− Якщо є метод, який повертає рядок і відомо, що результат завжди буде додано до StringBuffer, необхідно змінити реалізацію таким чином, щоб метод виконував додавання безпосередньо замість створення короткоживучого тимчасового об'єкта.
− При витяганні рядки з набору вхідних даних, необхідно повертати підрядок початкових даних замість створення копії. Буде створений новий об'єкт String, але масив символів для нього і початкових даних буде загальний. (Компроміс в тому, що якщо використовується тільки мала частина з початкового введення, то все одно воно буде зберігається в пам'яті цілком, якщо слідувати цій раді).
Більш радикальне: поділ багатовимірних масивів в один паралельний одновимірний масив:
− Масив int набагато краще, ніж масив Integer. Але можна узагальнити цей факт: два паралельних масиву int так само набагато ефективніше масиву об'єктів (int, int). Теж саме стосується будь-якої комбінації примітивів.
− Якщо потрібно реалізувати контейнер, який містить в собі пари (Foo, Bar), згадайте, що два паралельних масиву Foo[] і Bar[] загалом набагато краще, ніж один масив (Foo, Bar) об'єктів. (Виняток становить випадок, коли розроблюється API; для цих випадків краще триматися гарного API в невеликий збиток продуктивності. Але у внутрішньому коді варто намагатися бути якомога ефективніше.)
На пристроях без JIT виклик методів на об'єкті конкретного класу трохи швидше, ніж виклик через інтерфейс. (Таким чином дешевше викликати методи HashMap замість Map, навіть якщо це той самий об'єкт.) Реальне число близько до 6%. Більш того, з JIT різниці непомітно взагалі.
На пристроях без JIT кешування звернень до полів класу на 20% швидше повторювати звернення безпосередньо до поля. З JIT ціна звернення до поля дорівнює ціні звернення по локальній адресі, тому ця оптимізація виявляється не потрібна доти, поки не здається, що вона робить код читабельним. (Що є правдою щодо final, static і static final полів).
Якщо немає потреби в зверненні до полів об'єкта, то метод можна зробити статичним. Виклик стане на 15-20% швидше. Це добре і тому, що можна по сигнатурі сказати, що метод не змінює стану об'єкта.
У нативних мовах, таких як С++, хорошою практикою вважається використання геттерів (int i = getCount()) взамін прямого доступу (i = mCount). Це добре для С++, тому що компілятор зазвичай може виконати інлайн-підстановку і, якщо потрібно обмежити або налагодити звернення до поля, то можна додати потрібний код в будь-який час.
Так як Android -додатки створюються за домогою Java, то це значить що у компілятора не завжди є можливість робити інлайн-підстановку. Виклик віртуальних функцій у порівнянні до пошуку полів об’єкта - набагато дорожче. Використання загальних практик ООП і використання геттерів і сеттерів в інтерфейсі є обгрунтованим, але всередині класу завжди слід звертатися до полів безпосередньо.
Без JIT прямий доступ до поля приблизно в 3 рази швидше, ніж виклик звичайного геттеру. З JIT, де пряме звернення одне по швидкості прямим зверненням за локального адресою, прямий доступ буде приблизно в 7 разів швидше виклику методу-аксессору.
Використання static final для констант
static int intVal = 42;
static String strVal = "Hello, world!";
Компілятор генерує метод ініціалізації класу, з ім'ям, який виконується, коли клас використовується вперше. Метод привласнює значення 42 змінної intVal і витягує посилання з таблиці незмінних рядків клас-файлу для strVal. Коли відбудеться звертання до цих змінних, буде проведений пошук відповідних полів класу.
Правильнішим способом є використання «final» у змінних такого типу:
static final int intVal = 42;
static final String strVal = "Hello, world!";
Більше не потрібен метод, тому що константи записуються в статичні ініціалізатори полів у dex файлі. Код, який звертається до intVal, використовує ціле значення 42 безпосередньо, а доступ до strVal викличе недороге звернення до рядкової константи замість пошуку поля. Оголошення констант як static final слід використовувати скрізь, де це можливо.
Використання поліпшеного синтаксису циклу For
Цикл «for-each» може бути використаний для колекцій, які реалізують інтерфейс Iterable і масивів. Для колекцій виділяється ітератор в цілях виклику методів hasNext() і next(). Для ArrayList класичний цикл з лічильником приблизно в 3 рази швидше (з або без JIT), але для інших колекцій, «for-each» синтаксис буде еквівалентний явного використання ітератора.
Існує кілька альтернатив проходу по масиву:
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() виконується швидше. Він витягує потрібну інформацію в локальні змінні, уникаючи пошуку поля. Тільки array.length тут покращує продуктивність.
two() швидше для пристроїв без JIT і не відрізняється від one() для пристроїв з JIT. Він використовує розширений синтаксис for, представлений в Java 1.5
Використання 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);
}
}
Найголовніше, що варто відзначити, це визначення приватного внутрішнього класу (Foo$Inner), який безпосередньо звертається до приватного методу і приватне поле в зовнішньому класі. Код є правильним і виводить «Значення дорівнює 27».
Однак проблемою є те, що віртуальна машина вважає пряме звернення до приватних членам Foo з внутрішнього класу неприпустимим, тому що 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 замість private. Це означає, що поля будуть доступними з інших класів пакету, тому цей прийом не можна використовувати при побудові публічного API.
Використання чисел з плаваючою крапкою
Обчислення з плаваючою крапкою приблизно в 2 рази повільніше цілочисельних на пристроях Android.
У термінах швидкості, немає ніякої різниці між float і double на більш сучасному пристрої. По пам'яті, double в 2 рази більше.
Так само, деякі чіпи несуть на борту інтегроване множення цілих, але не мають інтегрованого цілочисельного ділення. У таких випадках, цілочисельне ділення та операції по модулю виконуються на рівні ПЗ.
Використання бібліотек
При використовуванні коду бібліотек замість написання власного, необхідно мати на увазі той факт, що система може замінити бібліотечний код на асемблерні вставки, які можуть бути швидше кращого коду, виробленого JIT -компілятором для Java -еквіваленту. Типовим прикладом може служити String.indexOf та інші методи, які Dalvik замінює на внутрішній код. Через це ж System.arraycopy приблизно в 9 разів швидше, ніж реалізований вручну цикл з JIT.
Дата добавления: 2015-12-07; просмотров: 81 | Нарушение авторских прав