Теория - Методы оптимизации ПО для современных микроконтроллеров - PRCY⮭net
Несмотря на то, что современные микропроцессоры и микроконтроллеры имеют все более высокую вычислительную мощность, процесс оптимизации остается таким же необходимым, как и прежде. Те задачи, которые раньше выполнялись за несколько часов, теперь выполняются за несколько микросекунд. Часто функции, написанные на языке высокого уровня, выполняются настолько быстро, что для них трудно определить какие-то временные рамки. Однако в результате выполнения большого количества вызовов той или иной функции незначительные изменения в скорости выполнения каждой из них становятся заметными. Очевидно, что необходима оптимизация программ, особенно разрабатываемых для применения во встроенных системах. Приложения для встроенных систем часто более требовательны к производительности процессора, в плане скорости, потребления и использования памяти, чем какие-либо другие типы приложений. Например, приложения общего назначения, обычно предъявляют требования к скорости выполнения в среднем случае, но, скажем, в системе цифровой обработки сигналов реального времени, требования к производительности предъявляются в каждом отдельном случае. Если программное обеспечение не достаточно быстро, оно считается неисправным. Даже если требования реального времени не критичны, оптимизация программного обеспечения позволит разработчику выбрать более медленный или дешевый процессор, предоставляя конкурентное преимущество на рынке.
В последнее время при разработке ПО для встраиваемых систем все чаще используется очень гибкий инструмент - язык С. Эффективность реализации алгоритма на С, влияет на эффективность кода сгенерированного компилятором. Существует большое количество наиболее общих и часто используемых методов оптимизации кода программного обеспечения, которые позволяют немного поправить ваш код и ускорить его выполнение. Применение методов специфичных для данного процессора или алгоритма позволит добиться более существенных результатов. Попытками оптимизации на данном уровне можно добиться определенного прироста в скорости выполнения, однако данные методы менее эффективны, чем выбор наилучшего подходящего алгоритма или удаление необязательных циклов.

Память может оказаться узким местом в архитектуре встроенной системы. Данную проблему можно решить, разместив наиболее часто используемые данные во внутренней, более быстрой памяти, а оставшиеся во внешней, более медленной. Самая быстрая память, это регистры, расположенные на кристалле процессора. Их всегда недостаточно, правильное распределение сильно влияет на общую производительность. Следующая по быстроте это кэш-память. В ней размещаются данные или команды, которые процессор предполагает использовать в следующие ближайшие моменты времени, таким образом, повышается к ним скорость доступа. Кэширование данных и команд, оказывает наибольшее воздействие на эффективность выполнения программы. Процесс заполнения внутренней памяти данными из внешней памяти требует большого времени. Удалось оценить, что каждый кэш-промах может привести к потере 10 20 тактов. Промах во внешней кэш-памяти ведет к потере, 20-40 циклов, а ошибка, вызванная отсутствием страницы, приводит к потере миллиона команд. Первое, что необходимо рассмотреть при оптимизации доступа к памяти это локальность обращений. На нее можно воздействовать изменением порядка, в котором осуществляется доступ, и размещаются объекты в памяти, или путем группировки всех часто используемых структур данных.
Предположим, у нас имеется массив целых значений размерности 1024Х1024, и мы собираемся подсчитать сумму значений в столбце, превышающих максимальную величину:
Код:
sum = 0;
for (i=0; i1024; i++)
{
if (array[i][K] MAX_VAL) sum += array[i][K];
}

В случае, если кэширование отсутствует, будет сгенерированна последовательность из 1024 циклов чтения перемешанных с выборкой команд (подразумевается отсутствие кэш-памяти команд). В том случае, если производится кэширование, каждое новое чтение будет приводить к кэш-промаху, запуская цикл ее заполнения из памяти. В большинстве случаев общее время выполнения суммирования велико.
Подсчет суммы значений в строке, превышающих максимальную величину:
Код:
sum = 0;
for (i=0; i1024; i++)
{
if (array[K][i] MAX_VAL) sum += array[K][i];
}
Этот кусок кода будет выполняться более быстро за счет эффекта кэширования. В любом случае, если кэш-память достаточно велика, чтобы разместить в ней весь массив данных, мы получим сходное увеличение производительности и при доступе к строкам и при доступе к столбцам массива. Если кэш слишком мала, то она может привести к уменьшению производительности. Поэтому очень важно использовать структуры данных с высокой локальностью обращений. Например, динамические связанные списки, могут уменьшить производительность программы, когда вы производите поиск данных в нем, или перемещаетесь к концу списка. Реализация списка на основе массива, может быть лучше и намного быстрее из-за лучшего кэширования, даже если учесть трудность изменения его размеров. Не следует полагать, что память под массив, выделяемая функцией malloc, будет размещена вплотную друг к другу при каждом последовательном вызове. Размещение одной сплошной большой области, должно быть осуществлено самостоятельно.
Еще одним фактором, влияющим на эффективность работы с памятью, является то, каким способом производится обращения к каждому элементу массива. Наиболее часто работа с массивами, производится внутри цикла. При этом в качестве управления циклом используют подсчет индекса до определенной величины. Этот же индекс используется для доступа к данным в одном или сразу нескольких массивах:
Код:
typedef struct
{
unsigned int a;
unsigned int b;
unsigned char c;
}str_t;

str_t sum[20];
str_t array1[20];
str_t array2[20];

for (ind = 0; ind 20; ind++)
{
sum[ind].a = array1[ind].a + array2[ind].a;
}
Время от времени и обычно в целях отличных от прохода через массив данных при помощи циклов, применяют указатели вместо оператора доступа к элементу массива. К элементам любого массива можно обратится через указатели. В итоге не трудно преобразовать исходный код, построенный на операторах доступа к элементу массива на тот, что базируется на работе с указателями:
Код:
for (ind = 0; ind 20; ++ind)
{
sum-a = array1-a + array2-a;
++array1;
++array2;
++sum;
}
Различия в этих двух фрагментах исходного кода не привлекают должного внимания и часто остаются не замеченными. Однако их эффективность значительно различается. Работа через указатели эффективнее. Многие компиляторы сгенерируют код, примерно, похожий на следующий:

Код:
//for (ind = 0; ind 20; ind++)
MOV.W #0x0, R9
//{
// sum[ind].a=array1[ind].a+array2[ind].a;
??main_0:
MOV.W R9, R12
MOV.W #0x6, R14
CALL #?Mul16
MOV.W array1(R12), R15
ADD.W array2(R12), R15
MOV.W R15, sum(R12)
//}
ADD.W #0x1, R9
CMP.W #0x14, R9
JNC ??main_0

Код:
//for (t = 0; t count; t++)
MOV.B #0x14, R14
//{
// sum-a = array1-a + array2-a;
??main_1:
MOV.W @R10, R15
ADD.W @R11, R15
MOV.W R15, 0(R8)
// ++array1;
ADD.W #0x6, R10
// ++array2;
ADD.W #0x6, R11
// ++sum;
ADD.W #0x6, R8
//}
ADD.B #0xff, R14
JNE ??main_1

При каждом обращении к элементу массива, индекс цикла переводится в смещение относительно его начала и после этого добавляется к начальному адресу массива. при каждом обращении к его элементу. В случае, если размер элемента массива равен степени двойки, преобразование индекс-смещение простое. Чрезмерные накладные расходы будут появляться в том случае, если размер элемента массива не равен степени двойки. В этом случае при преобразовании будет вызываться команда умножения, либо специальная функция как в случае с некоторыми микроконтроллерами MSP430.
Доступ к массиву через указатели лишен данного недостатка. В данном случае указатель просто увеличивается постоянно, на величину 6 байт. При этом не имеет значения размер элемента массива, и тот факт, поддерживает ли микроконтроллер операцию умножения на аппаратном уровне.

Благодарю за внимание, продолжение следует!..
Information
  • Posted on 31.01.2010 22:06
  • Просмотры: 2088