Оптимізація коду під Pebble


Примітка. Відповідна стаття на хабр писалася українською, ось цей оригінал.
На хабрі вже було декілька статей про загальні принципи написання коду для Pebble. Для програмування використовується мова C, а сам процес розробки відбувається у браузері, при цьому компіляція відбувається на віддалених серверах, і змінити її параметри немає можливості, хіба що встановити Ubuntu і інсталювати необхідні інструменти для офлайн-компіляції. Та навіть такий хід не позбавить основного обмеження — на пристрої доступно тільки 24 Кб оперативної пам’яті, яка використовується і під скомпільований код, тобто справді динамічної пам’яті залишається 8-10 Кб. Якщо для простих програм, які використовуються як тонкі клієнти або додаткові датчики для телефону, цього з головою достатньо, то для написання самодостатньої більш-менш складної гри, якій не потрібен смартфон, цього замало. Ось тут і знадобиться оптимізація коду під розмір.
Свої ґулі я вже набила, і тому пропоную повчитися на моїх помилках, які я об’єднала у 16 порад. Деякі з них можуть здатися капітанським, від деяких позбавить хороший компілятор із правильними флагами компіляції, але, сподіваюся, деякі з них комусь та й будуть корисними.

У багатьох років 10 тому були телефони Siemens, і, напевно, багато хто грав у гру Stack Attack, яка часто була встановлених. Процесор з частотою 26МГц у власника сучасного смартфону викликає посмішку. Але, незважаючи на дуже слабке за нинішніми мірками залізо, ці стародавні чорно-білі телефони підтримували Java-ігри, якою і є Stack Attack 2 Pro.
Згадала про цю гру я тоді, коли у мене з'явився pebble. Його залізо на порядок потужніше тих старих телефонів, але екран майже такий же. Після простих тестів виявилося, що цей екран цілком може показувати 60 кадрів на секунду. Більш-менш складні ігри в Pebble Appstore можна перерахувати на пальцях, тому вирішено було писати Stack Attack для Pebble.
Пошук скріншотів, з яких можна було б отримати нормальні ресурси для гри, нічого не дав. Тому я знайшла емулятор Siemens C55 на старому-престарому сайті, і саму гру. Так вдалося згадати, як же виглядала гра. А після колупання в jar-архіві вдалося відносно просто дістати картинки і тексти.
Для тих, хто не хоче встановлювати всілякі незрозумілі емулятори (які, як не дивно, навіть на Windows 8 майже без проблем запускаються), я записала ностальгічні відео:



1. Перший, і найочевидніший спосіб — використовуйте inline всюди, де є така можливість. Якщо функція викликається рівно 1 раз, то це дозволить зекономити 12 байт. Але якщо функція не є тривіальною, то можна також добряче залетіти, тому будьте обережні. Також мінусом цього способу є те, що в більшості випадків доведеться писати код у .h-файлі.
2. Як би банально це не звучало, пишіть менше коду, допоки це не заважає його нормально читати. У загальному випадку, менше коду — менше бінарний файл.
3. Переносьте всі тексти у файли ресурсів. Програма для pebble може містити близько 70 Кб ресурсів, чого цілком достатньо, якщо ваша програма не показує нову картинку щохвилини.
Зазвичай усі тексти не відображаються одразу, тому використання динамічної пам’яті замість статичної дозволить зекономити місце. Недоліком є те, що доведеться писати зайвий код, який підвантажує і вивантажує ресурси за їх ідентифікаторами. Також може здатися, що читабельність коду від цього постраждає, утім, це не завжди так. Як приклад наведу код зі своєї гри (вгорі) та аналогічну ділянку коду із тестового проекту (знизу):
static void cranes_menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data)
{
int const index = cell_index->row;
menu_cell_basic_draw( ctx, cell_layer, texts[ index * 2 ], texts[ index * 2 + 1 ], NULL );
}

static void menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) {
switch (cell_index->row) {
case 0:
menu_cell_basic_draw(ctx, cell_layer, «Basic Item», «With a subtitle», NULL);
break;
case 1:
menu_cell_basic_draw(ctx, cell_layer, «Icon Item», «Select to cycle», NULL);
break;
}
}

4. Замість звільнення кожного із ресурсів окремо, використовуйте масиви ресурсів і звільняйте їх у циклі. При трьох і більше ресурсах такий підхід дозволяє економити пам’ять.
Приклад
for ( int i=0; i<7; ++i ) {
gbitmap_destroy( s_person_images[i] );
}
краще, ніж
gbitmap_destroy( s_person1_image );

gbitmap_destroy( s_person7_image );

5. Уникайте зайвих змінних там, де це здається доцільним. Наприклад, код
for (int i=0; i<9; ++i)
{
for (int k=0; k<3; ++k)
{
btexts[i/3][i%3][k] = master[count];
count++;
}
}

займає на 20 байт менше, ніж
for (int i=0; i<3; i++)
{
for (int j=0; j<3; j++)
{
for (int k=0; k<3; k++)
{
btexts[i][j][k] = master[count];
count++;
}
}
}

6. Якщо код уже неможливо прочитати просто, пишіть найоптимальніше. Для прикладу, ось частина коду із проекту tertiary_text:
size /= 3;
if (b == TOP)
end -= 2*size;
else if (b == MID)
{
top += size;
end -= size;
}
else if (b == BOT)
top += 2*size;

Цей код робить те ж, що й
size /= 3;
top += b*size;
end -= (2-b)*size;

Верхній і нижній код відрізняються по розміру в кілька разів, і, як на мене, читабельність у них однаково низька.
7. Використовуйте enum для того, щоб перенести послідовні виклики в цикл. Більш того, за рахунок процесорної магії такий код може працювати навіть швидше.
unsigned char RESOURCE_ID_BOXES[11] = { RESOURCE_ID_BOX1, RESOURCE_ID_BOX2, RESOURCE_ID_BOX3, RESOURCE_ID_BOX4, RESOURCE_ID_BOX5,
RESOURCE_ID_BOX6, RESOURCE_ID_BOX7, RESOURCE_ID_BOX8, RESOURCE_ID_BOX9, RESOURCE_ID_BOX10,
RESOURCE_ID_BOX11 };
for (int i=0; i<11; ++i) {
s_boxes_bitmap[i] = gbitmap_create_with_resource( RESOURCE_ID_BOXES[i] );
}

замість
s_boxes_bitmap[0] = gbitmap_create_with_resource( RESOURCE_ID_BOX1 );

s_boxes_bitmap[10] = gbitmap_create_with_resource( RESOURCE_ID_BOX11 );

8. Коли я вперше після довгих років побачила цю картинку:

я подумала: от уміли ж раніше робити! Ось тут, якщо придивитися, фон циклічний, ось тут циклічно, і тут. Економили пам’ять, як могли. Ось іще стаття про те, як економили пам’ять на однакових хмарах і кущах.
Насправді, коли я розпакувала ресурси, то побачила, що весь фон зроблено однією картинкою. Спочатку я зробила так само — і одразу втратила близько 2Кб оперативної пам’яті там, де можна було б обійтися вчетверо меншим об’ємом.
Отож, сама порада: використовуйте зображення якомога меншого розміру, бо кожне з них висить в оперативній пам’яті. Промальовуйте програмно все, що тільки можете, на щастя, процесорної потужності вистачає на 60 кадрів на секунду.
За рахунок того, що малювати можна до 60 кадрів на секунду без жодних проблем, є можливість відображати разом із чорним та білим ще й сірий колір. Я швиденько написала тестову програму (github), яка це демонструє, утім, реального використання цієї можливості я не бачила. У першій-ліпшій програмі, яка демонструє зображення з камери на pebble, цього не було.

Я розділила фон на частини, які циклічно повторюються. Pebble автоматично повторює зображення, якщо прямокутник, у якому воно повинно відображатися, більший, ніж саме зображення, і це варто використовувати. Але якщо переборщити і малювати зображення розміром 1x1 на весь екран, fps буде дуже низьким.
9. Робіть ресурси такими, щоб їх можна було використовувати «з коробки», тобто без написання додаткового коду.
У моїй грі персонаж може ходити ліворуч і праворуч, зображення при цьому симетричні. Спочатку я думала зекономити місце, тому написала код, який відображав зображення дзеркально. Але після того, як пам’яті перестало вистачати, від цього коду довелося відмовитися.
10. Уникайте «довгоживучих» ресурсів там, де це не виправдано. Якщо після вибору режиму гри меню більше не використовуватиметься, знищуйте його одразу перед грою. Якщо використовуватиметься — запам’ятайте його стан і відтворіть тоді, коли це буде необхідно. Якщо картинка відображається тільки при старті, видаляйте її відразу після показу.
11. Використовуйте static-методи і static-змінні, використовуйте const скрізь, де змінну не передбачається змінювати.
static const char caps[] = «ABCDEFGHIJKLM NOPQRSTUVWXYZ»;

краще, ніж просто
char caps[] = «ABCDEFGHIJKLM NOPQRSTUVWXYZ»;

12. Використовувати один і той же callback там, де це можливо. Наприклад, якщо у двох меню menu_draw_header_callback порожній, немає сенсу писати його двічі.
static void menu_draw_header_callback(GContext* ctx, const Layer *cell_layer, uint16_t section_index, void *data)
{
}
menu_layer_set_callbacks(menu_layer, NULL, (MenuLayerCallbacks) {
.get_num_rows = menu_get_num_rows_callback,
.draw_header = menu_draw_header_callback,
.draw_row = menu_draw_row_callback,
.select_click = select_callback,
});

13. Використовувати user_data тих об’єктів, які його мають. Пам’ять уже виділена, чому б не скористатися нею у своїх цілях?
14. Використовуйте int як основний тип, навіть якщо слід перерахувати від 0 до 5. Думаю, виграш пов’язаний із тим, що компілятор вставляє додатковий код, якщо використовуються менші типи.
15. Намагайтеся повторно використовувати код максимальну кількість разів.
Ця порада схожа на пораду №12, але більш загальна. Не використовуйте метод копіпасту зі зміною кількох рядків коду після цього, замість цього використайте якийсь флаг, який би передавався у функцію.
16. Остання порада найнебезпечніша серед усіх попередніх. Одразу попереджу, що я нею не користувалася, і нікому користуватися не рекомендую. Утім, бувають ситуації, коли іншого виходу немає. Для того, щоб її випадково не прочитали діти за вашою спиною, ховаю її під спойлер.
Не звільняйте ресурси. Інколи це може не мати наслідків, наприклад, якщо ресурси знищуються тільки при завершенні програми. Але потенційно це призведе до нестабільної роботи і вильотів, які дуже складно відслідкувати. Pebble виводить у логи кількість зайнятої пам’яті після завершення програми. Бажаю вам, щоб там завжди було 0b.
Якщо ваша програма використовує rand(), то після виходу може залишатися 24 незвільнених байти. Цьому багу вже близько року. Для себе я вирішила цю проблему наступним кодом:
int _rand(void) /* RAND_MAX assumed to be 32767. */
{
static unsigned long next = 1;
next = next * 1103515245 + 12345;
return next >> 16;
}



Результат
Гра доступна в pebble appstore, код викладено на github. Ось відео того, що вийшло:

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.