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

в 15:37, , рубрики: Pebble, Блог компании EPAM Systems, ненормальное программирование

Оптимизация кода под Pebble - 1На Хабре уже было несколько статей об общих принципах написания кода под Pebble. Для программирования используется язык C, а сам процесс разработки происходит в браузере, при этом компиляция происходит на удаленных серверах, и изменить ее параметры нет возможности, разве что установить Ubuntu и инсталлировать необходимые инструменты для офлайн-компиляции. Но даже такой ход не избавит основного ограничения – на устройстве доступно только 24 Кб оперативной памяти, которая используется и для скомпилированного кода, то есть действительно динамической памяти остается 5-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. Когда я впервые после долгих лет увидела эту картинку:
Оптимизация кода под Pebble - 2
я подумала: вот умели же раньше делать! Вот здесь, если присмотреться, фон циклический, вот здесь циклически, и здесь… Экономили память, как могли! Вот еще статья о том, как экономили память на одинаковых облаках и кустах.
На самом деле, когда я распаковала ресурсы, то увидела, что весь фон сделан одной картинкой. Сначала я сделала так же – и сразу потеряла около 2Кб оперативной памяти там, где можно было бы обойтись вчетверо меньшим объемом.
Итак, сам совет: используйте изображения как можно меньшего размера, ибо каждое из них «висит» в оперативной памяти. Прорисуйте программно все, что только можно, к счастью, процессорной мощности хватает на 60 кадров в секунду.

Читерство с использованием 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. Последний совет опаснее всех предыдущих. Сразу предупрежу, что я им не пользовалась, и никому пользоваться не рекомендую. Впрочем, бывают ситуации, когда другого выхода нет. Для того, чтобы его случайно не прочли дети за вашей спиной, прячу совет под спойлер.

Если вам уже есть 18

Не освобождает ресурсы. Иногда это может не иметь последствий, например, если ресурсы уничтожаются только при завершении программы. Но потенциально это приведет к нестабильной работе и вылетов, которые очень сложно отследить. Pebble выводит в логи количество занятой памяти после завершения программы. Желаю вам, чтобы там всегда было 0b.

Спойлер про 24 байти

Если программа использует 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. Вот видео того, что получилось:

Автор: svitlana_tsymbaliuk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js