Совсем недавно стукнул год, с тех пор как на просторах AppleStore появилось наше первое приложение. По началу было довольно сложно во всем разобраться. Особенно если учесть, что до этого я разработкой приложений под MacOS не занимался. За этот год много чего было написано. Приложения, которые мы написали, я к сожалению назвать не могу (не помню всех, да и руководство не одобряет такие вещи), но о нескольких способах оптимизации приложений под данную платформу я могу вам смело рассказать.
Где-то пол года (или даже больше) назад пришлось мне писать приложение основной задачей которого была обработка звука. Для этого был написан свой несложный движок, который все это делал. Приложение было выпущено в свет и постепенно этот движок начал часто использоваться в других приложениях подобного рода. Но вот недавно началась разработка 2-й версии этой программы. Требования возросли, а ресурсы стареньких ифонов не изменились. Вот тут и пришлось поискать пути улучшения уже написанного кода.
Первое что приходит в голову — попытаться выжать все что умеет компилятор. Пожалуй самый главный параметр, который тут можно поменять — компиляция приложения под режим thumb. Если оный режим включен — то для выполнения наших задач будет использоваться сокращенный набор команд. Этот набор команд будет кодироваться более компактным кодом, но и использовать все ресурсы процессора мы не сможем. В частности использовать VFP напрямую не получится. В местах где мы проводим операции над числами с плавающей точкой можно встретить код наподобие такого:
После компиляции это будет выглядеть примерно так:
Не в режиме thumb код будет таким:
Как видим разница довольно существенная. Отсутствует лишний вызов функции и все операции с плавающей точкой происходят на месте, а не где-то далеко и не у нас. Наверно у вас возникнет вопрос а быстрее ли это работает? Ответ естественно да, работает это быстрее. Хотя если ваша программа не проводит тяжелые расчеты — то и так сойдет. Как плюс режиму thumb — более компактый код, а значит теоретически программа будет загружаться быстрее.
Кстати в XCode tools можно задавать каждому файлу персональные параметры, и режим thumb можно выключить (или наоборот включить) только для отдельных фрагментов проекта, что довольно удобно.
Следующй этап для ускорения расчетов — выкинуть как можно больше операций с плавающей точкой. Вместо этого преобразовывать наши числа в целые, умноженные на нейкий коэфффициент. Естественно коэффициент лучше выбирать кратным степени 2-йки, чтобы было потом удобно получить нужные данные.
Ну вот мы заставили компилятор использовать все ресурсы процессора в местах где это важно, избавились по возможности от операций с плавающей точкой. Теперь давайте глянем на спеку по ArmV6 (например тут). Если внимательно почитать описания функций то можно там увидеть очень много интересных команд (многие из них тоже не доступны в режиме thumb).
Например есть у вас задача сделать простой ФНЧ или ФВЧ. Алгоритм в итоге сводится к расчету вот такой формулы:
(b0,b1,b2,a1,a2 — это константы при заданной частоте отсечки)
А теперь загляните в описание команды «smlad». Эта команда делат умножение 2-х 16-ти битных чисел, суммирует результаты и указанный вами регистр. Формулой это будет выглядеть так (в квадратных скобках указаны биты):
Т.е. сам расчет нашей формулы можно сделать в 3 операции. Осталось только решить вопрос как же использовать эту функцию. Благо опыта с ассемблером у меня много еще с времен Доси, а в gcc просто замечательно работаю вставки написанные на ассеблере. Вобщем напишем функцию, которая будет использовать данную команду:
Кстати для удобства можно сделать версию функции для симулятора. А то тестить будет не удобно. У меня это получилось вот так:
В итоге расчет нашей формулы будет выглядеть так:
Заглянем в дизасм:
Все красиво и понятно. Как сказал один мой знакомый «загрузил. выполнил. загрузил. выплюнул». То что было до этого лучше не смотреть. Там был просто ужас. Итак у меня в программе было 2 канала, на которых был эффект задержки. На каждый такой эффект нужно было 2 фильтра (один ФНЧ, другой ФВЧ). Итого 4 фильтра. После оптимизации, глянув в Instruments загрузку процессора — видим что вместо ~45% программа кушает ~35% процессорного времени. Довольно не плохой результат :)
Кстати, почитав документацию я с удивлением обнаружил отсутствие целочисленного деления. В итоге немного модифицировав алгоритм линейной интерполяции (используется при ресемплинге на всех активных каналах) загрузка вообще упала до ~30% :)
Вот так пару простых и довольно очевидных оптимизаций снизили загрузку процессора примерно на 1/3.
P.S. Тестировалось все на iPhone 3g.
Где-то пол года (или даже больше) назад пришлось мне писать приложение основной задачей которого была обработка звука. Для этого был написан свой несложный движок, который все это делал. Приложение было выпущено в свет и постепенно этот движок начал часто использоваться в других приложениях подобного рода. Но вот недавно началась разработка 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.