Pull to refresh

Оптимизация приложений (Iphone armv6)

Reading time6 min
Views1.2K
    Совсем недавно стукнул год, с тех пор как на просторах AppleStore появилось наше первое приложение. По началу было довольно сложно во всем разобраться. Особенно если учесть, что до этого я разработкой приложений под MacOS не занимался. За этот год много чего было написано. Приложения, которые мы написали, я к сожалению назвать не могу (не помню всех, да и руководство не одобряет такие вещи), но о нескольких способах оптимизации приложений под данную платформу я могу вам смело рассказать.
    Где-то пол года (или даже больше) назад пришлось мне писать приложение основной задачей которого была обработка звука. Для этого был написан свой несложный движок, который все это делал. Приложение было выпущено в свет и постепенно этот движок начал часто использоваться в других приложениях подобного рода. Но вот недавно началась разработка 2-й версии этой программы. Требования возросли, а ресурсы стареньких ифонов не изменились. Вот тут и пришлось поискать пути улучшения уже написанного кода.


Настройки компилятора (thumb)



    Первое что приходит в голову — попытаться выжать все что умеет компилятор. Пожалуй самый главный параметр, который тут можно поменять — компиляция приложения под режим thumb. Если оный режим включен — то для выполнения наших задач будет использоваться сокращенный набор команд. Этот набор команд будет кодироваться более компактным кодом, но и использовать все ресурсы процессора мы не сможем. В частности использовать VFP напрямую не получится. В местах где мы проводим операции над числами с плавающей точкой можно встретить код наподобие такого:

double prevTime=CFAbsoluteTimeGetCurrent();
{
  ...
}
double nextTime=CFAbsoluteTimeGetCurrent();
double dt = nextTime-prevTime;
printf("dt=%f",dt);


* This source code was highlighted with Source Code Highlighter.


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

blx  L_CFAbsoluteTimeGetCurrent$stub
mov  r5, r1
blx  L_CFAbsoluteTimeGetCurrent$stub
mov  r3, r5
mov  r2, r4
blx  L___subdf3vfp$stub
ldr  r6, L7
mov  r2, r1
mov  r1, r0
mov  r0, r6
blx  L_printf$stub


* This source code was highlighted with Source Code Highlighter.


    Не в режиме thumb код будет таким:

bl  L_CFAbsoluteTimeGetCurrent$stub
fmdrr  d8, r0, r1
bl  L_CFAbsoluteTimeGetCurrent$stub
fmdrr  d6, r0, r1
ldr  r0, L7
fsubd  d7, d6, d8
fmrrd  r1, r2, d7
bl  L_printf$stub


* This source code was highlighted with Source Code Highlighter.


    Как видим разница довольно существенная. Отсутствует лишний вызов функции и все операции с плавающей точкой происходят на месте, а не где-то далеко и не у нас. Наверно у вас возникнет вопрос а быстрее ли это работает? Ответ естественно да, работает это быстрее. Хотя если ваша программа не проводит тяжелые расчеты — то и так сойдет. Как плюс режиму thumb — более компактый код, а значит теоретически программа будет загружаться быстрее.
    Кстати в XCode tools можно задавать каждому файлу персональные параметры, и режим thumb можно выключить (или наоборот включить) только для отдельных фрагментов проекта, что довольно удобно.

Оптимизация алгоритма


    Следующй этап для ускорения расчетов — выкинуть как можно больше операций с плавающей точкой. Вместо этого преобразовывать наши числа в целые, умноженные на нейкий коэфффициент. Естественно коэффициент лучше выбирать кратным степени 2-йки, чтобы было потом удобно получить нужные данные.
    Ну вот мы заставили компилятор использовать все ресурсы процессора в местах где это важно, избавились по возможности от операций с плавающей точкой. Теперь давайте глянем на спеку по ArmV6 (например тут). Если внимательно почитать описания функций то можно там увидеть очень много интересных команд (многие из них тоже не доступны в режиме thumb).
    Например есть у вас задача сделать простой ФНЧ или ФВЧ. Алгоритм в итоге сводится к расчету вот такой формулы:
    tmp = b0*in0+b1*in1+b2*in2 -a1*out1-a2*out2;

* This source code was highlighted with Source Code Highlighter.

(b0,b1,b2,a1,a2 — это константы при заданной частоте отсечки)

    А теперь загляните в описание команды «smlad». Эта команда делат умножение 2-х 16-ти битных чисел, суммирует результаты и указанный вами регистр. Формулой это будет выглядеть так (в квадратных скобках указаны биты):

    result[0:31] = a[0:15]*b[0:15] + a[16:31]*b[16:31] + с[0:31]

* This source code was highlighted with Source Code Highlighter.


    Т.е. сам расчет нашей формулы можно сделать в 3 операции. Осталось только решить вопрос как же использовать эту функцию. Благо опыта с ассемблером у меня много еще с времен Доси, а в gcc просто замечательно работаю вставки написанные на ассеблере. Вобщем напишем функцию, которая будет использовать данную команду:

inline volatile int SignedMultiplyAccDual(int32_t x, int32_t y, int32_t addVal)
{
  register int32_t result;
  asm volatile("smlad %0, %1, %2, %3"
         : "=r"(result)
         : "r"(x), "r"(y), "r"(addVal)
        );
  return result;
}


* This source code was highlighted with Source Code Highlighter.


    Кстати для удобства можно сделать версию функции для симулятора. А то тестить будет не удобно. У меня это получилось вот так:

#if defined __arm__
inline volatile int SignedMultiplyAccDual(int32_t x, int32_t y, int32_t addVal)
{
  register int32_t result;
  asm volatile("smlad %0, %1, %2, %3"
         : "=r"(result)
         : "r"(x), "r"(y), "r"(addVal)
         );
  return result;
}

inline volatile int SignedMultiplyAcc(int32_t x, int32_t y, int32_t addVal)
{
  register int32_t result;
  asm volatile("mla %0, %1, %2, %3"
         : "=r"(result)
         : "r"(x), "r"(y), "r"(addVal)
          );
  return result;
}

#else

inline volatile int SignedMultiplyAcc(int32_t x, int32_t y, int32_t addVal)
{
  register int32_t result;
  result = x*y+addVal;
  return result;
}

inline volatile int SignedMultiplyAccDual(int32_t x, int32_t y, int32_t addVal)
{
  register int32_t result;
  result = int16_t(x & 0x0000FFFF) * int16_t(y & 0x0000FFFF);
  result += int16_t(x >> 16) * int16_t(y >> 16);  
  result += addVal;
  return result;
}
#endif  


* This source code was highlighted with Source Code Highlighter.


    В итоге расчет нашей формулы будет выглядеть так:
  tmp = fParamsHigh[0]*fValsHigh[0];
  tmp = SignedMultiplyAccDual(*(int32_t *)&fParamsHigh[1],*(int32_t *)&fValsHigh[1],tmp);
  tmp = SignedMultiplyAccDual(*(int32_t *)&fParamsHigh[3],*(int32_t *)&fValsHigh[3],tmp);
  tmp = tmp >> PARAMS_SHL_VAL; 


* This source code was highlighted with Source Code Highlighter.


    Заглянем в дизасм:
ldrh  r3, [r4, #196]
ldrh  r0, [r4, #206]
ldr  r2, [r4, #208]
smulbb  r3, r3, r0
smlad r3, r1, r2, r3
ldr  r1, [r4, #202]
ldr  r2, [r4, #212]
smlad r3, r1, r2, r3
mov  r3, r3, asr #10


* This source code was highlighted with Source Code Highlighter.


    Все красиво и понятно. Как сказал один мой знакомый «загрузил. выполнил. загрузил. выплюнул». То что было до этого лучше не смотреть. Там был просто ужас. Итак у меня в программе было 2 канала, на которых был эффект задержки. На каждый такой эффект нужно было 2 фильтра (один ФНЧ, другой ФВЧ). Итого 4 фильтра. После оптимизации, глянув в Instruments загрузку процессора — видим что вместо ~45% программа кушает ~35% процессорного времени. Довольно не плохой результат :)
    Кстати, почитав документацию я с удивлением обнаружил отсутствие целочисленного деления. В итоге немного модифицировав алгоритм линейной интерполяции (используется при ресемплинге на всех активных каналах) загрузка вообще упала до ~30% :)
  Вот так пару простых и довольно очевидных оптимизаций снизили загрузку процессора примерно на 1/3.
P.S. Тестировалось все на iPhone 3g.
Tags:
Hubs:
Total votes 24: ↑21 and ↓3+18
Comments11

Articles