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


    На Хабре уже было несколько статей об общих принципах написания кода под 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. Когда я впервые после долгих лет увидела эту картинку:



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

    На самом деле, когда я распаковала ресурсы, то увидела, что весь фон сделан одной картинкой. Сначала я сделала так же – и сразу потеряла около 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.

    Вот видео того, что получилось:

    EPAM
    165.46
    Компания для карьерного и профессионального роста
    Share post

    Comments 16

      +1
      14. Используйте int как основной тип, даже если следует пересчитать от 0 до 5. Думаю, выигрыш связан с тем, что компилятор вставляет дополнительный код, если используются меньшие типы.

      Верно, в арифметических операциях над операндами меньших чем int типов, операнды приводятся к int перед выполнением операции, результат приводится к нужному типу после ее выполнения.
        +2
        Эти бы слова, да разработчикам Android…

        Не знаком с платформой, но если в качестве компилятора используется gcc, можно тюнить параметры компилятора прямо в сходном коде. В частности, всякие pragma optimize и атрибуты функций. Возможно там и так стоит -Os, но данным способом можно для некоторых «горячих точек» скорости добавить или наоборот.
          0
          Спасибо огромное, это прекрасно! Я даже думал о покупке такого Siemens'а на eBay. А тут как раз черно-белый экран, оригинальная графика… только подсветка экрана не оранжевая :D

          P.S. Оно… работало… на джаве? o_0
            0
            Да, оно работало на Java, точнее J2ME. Вы можете скачать эмулятор и саму игру по ссылкам из статьи. Возможно, даже получится декомпилировать и посмотреть, что внутри.
            • UFO just landed and posted this here
                0
                Многие телефоны, несмотря на отсутствие возможности заливки пользовательских j2me-приложений, имели-таки жаву для запуска предустановленных программ/игр.
                • UFO just landed and posted this here
            +1
            Раз коснулись rand(), после localtime() остается 40b.
              0
              По моему небольшому опыту программирования Pebble, потребляемая мощность гораздо важнее экономии памяти. Программная прорисовка на 60 кадрах в секунду это, наверное, лучший способ быстро посадить батарейку.
                +2
                Быстрее всего батарейку при игре садит подсветка. Да и вряд ли пользователь ожидает, что в таком режиме часы, простите за каламбур, проживут дольше пары часов.
                Кстати, вы можете попробовать изменить время тика и, соответственно, FPS. Для этого скомпилируйте проект, изменив в stack_attack.c:10
                #define FPS 60
                

                Но учтите, что больше 60 раз на секунду pebble изменять содержимое экрана не позволяет.
                  0
                  Подсветка, подтверждаю. Из-за того, что в экране есть отражающая пленка для хорошей видимости на солнце, он практически непрозрачный на просвет. Соответственно для подсветки там используются довольно яркие светодиоды.
                0
                Во, уже промежуточные оттенки генерируют. Скоро кто-нибудь найдёт заводской глюк, вызывающий кратковременные радужные разводы на жидкокристаллическом экранчике, и через некоторое время у нас будут программно контролируемые цветные режимы и подобные радости: www.studiostyle.sk/dmagic/gallery/gfxmodes.htm
                  +1
                  Кратковременные? Вы про пластиковую версию? Это из-за поляризации в материале пластика корпуса, сделать с этим ничего не получится.
                  0
                  О да!

                  Помню, в этой игре был багфича: каким-то образом можно было тянуть блоки на себя, и если сделать это у края экрана, можно было за этот край вылезти и сидеть там бесконечно долго, наблюдая за падением блоков. Вы его воспроизвели?
                    +3
                    Вы, наверное, путаете черно-белую игру от Mophun и одного из ее, на мой взгляд неудачных, наследников. Действительно, подобная игра (Stack Attack Junior) была и на Siemens A60, например. Телефон Java не поддерживал, но по железу был идентичен Siemens C60, который ее поддерживал. Вполне допускаю, что возможность была програмно заблокированной, и игра под А60 была тоже написаной на Java, по крайней мере один признак Java-игр у нее точно был — она очень медленно работала. Так вот, в последующих играх серии Stack Attack были другие бонусы, в том числе и «Вытянуть ящик из стопки» у одного из персонажей. Я же реализовала только те, что были в игре для ч/б Siemens'ов: бомба, большой прыжок (J), каска (P), дополнительная жизнь (L) и открытие персонажа (точка). Но всегда можно создать форк на github!
                    0
                    Ностальгический Stack Attack! Божественно!
                    Не так давно начал писать Stack Attack для андроид, но к сожалению, не хватило времени закончить.

                    Only users with full accounts can post comments. Log in, please.