Программирование DSP VisualDSP: управление оптимизацией кода Fri, December 13 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


VisualDSP: управление оптимизацией кода Печать
Добавил(а) microsin   

Через контекстное меню каждого модуля проекта можно задать для каждого модуля индивидуальные настройки компиляции (File Options...). У тех модулей, которых применены индивидуальные настройки, отличающиеся от глобальных настроек проекта, на иконке файла стоит восклицательный знак.

Также можно управлять оптимизацией директивами #pragma

[Директивы #pragma, управляющие оптимизацией цикла

Прагмы оптимизации цикла дают компилятору дополнительную информацию по использованию оптимизации внутри отдельного цикла, что позволяет компилятору выполнить более агрессивную оптимизацию. Эти прагмы помещаются перед оператором цикла, и применяется к оператору, который сразу следует за прагмой. Этот оператор должен быть оператором for, while или do. Обычно наиболее эффективны прагмы для самых вложенных циклов, поскольку компилятор в этом месте может получить наибольшее управление операциями.

Оптимизатор всегда пытается векторизировать циклы, когда есть возможность это сделать безопасно. Оптимизатор использует информацию, сгенерированную межпроцедурным анализом (interprocedural analysis), чтобы повысить количество случаев, когда есть информация о том, что можно безопасно делать оптимизацию (см. раздел "Interprocedural Analysis" в руководстве [1]).

Рассмотрим следующий код:

void copy(short *a, short *b) {
   int i;
   for (i=0; i < 100; i++)
      a[i] = b[i];
}

Если Вы вызвали функцию copy двумя вызовами, наподобие copy(x,y) и затем copy(y,z), то межпроцедурный анализ не сможет сказать, что "a" не является псевдонимом "b". Таким образом, оптимизатор не уверен, что одна итерация цикла не зависит от данных, вычисленных предыдущей итерацией цикла. Если известно, что каждая итерация цикла не зависит от предыдущей итерации, то может использоваться прагма vector_for для явного оповещения компилятора о том, что это как раз тот случай.

Прагма all_aligned применяется к последующему циклу. Эта прагма утверждает, что все указатели выровнены к самой желательной границе.

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

Прагма extra_loop_loads инструктирует компилятор, что в немедленно следующим за ней цикле разрешено выполнять дополнительные чтения за концом указанных областей памяти, так что если цикл сделает дополнительную итерацию, то это позволит компилятору генерировать более эффективный код. Например,

short dotprod_normal(int n, short *x, short *y)
{
   int i;
   short sum = 0;
#pragma no_vectorization
   for (i = 0; i < n; i++)
      sum += x[i] * y[i];
   return sum;
}
 
short dotprod_with_pragma(int n, short *x, short *y)
{
   int i;
   short sum = 0;
#pragma no_vectorization
#pragma extra_loop_loads
   for (i = 0; i < n; i++)
      sum += x[i] * y[i];
   return sum;
}

Эти примеры используют прагму no_vectorization, чтобы заставить компилятор генерировать упрощенные версии функции. Без прагмы no_vectorization, компилятор генерирует векторизированные и не векторизированные версии цикла, который не лишает действия прагмы extra_loop_loads pragma, но делает пример боле сложным для реализации.

В следующем примере функция dotprod_normal() только читает элементы массива x[0]..x[n-1] и y[0]..y[n-1], используя следующий код:

_dotprod_normal:
      P1 = R2 ;
      P2 = R0 ;
      CC = R0 < = 0;
      R0 = 0;
      IF CC JUMP ._P2L8 ;
      I0 = R1 ;
      P2 += -1;
      LSETUP (._P2L5 , ._P2L6-8) LC0 = P2;
      CC = P2 == 0;
      MNOP || R0 = W[P1++] (X) || R1.L = W[I0++];
      IF CC JUMP ._P2L6 ;
.align 8;
._P2L5:
      A0 += R0.L*R1.L (IS) || R0 = W[P1++] (X) ||
      R1.L = W[I0++];
._P2L6:
      A0 += R0.L*R1.L (IS);
      R0 = A0.w;
      R0 = R0.L (X);
._P2L8:
      RTS;

Для улучшения быстродействия компилятор планирует чтения из x[i+1] и y[i+1] параллельно к сложению x[i] и y[i]. Это можно сделать только для n-1 итераций, и компилятор выполняет n-1 итераций цикла, и делает n-тое добавление после того, как цикл завершится. Поскольку значение n не известно, компилятор должен вычислить n-1, и перед входом в цикл проверить, что результат не 0.

Сравните с кодом, который сгенерировал компилятор для функции dotprod_with_pragma():

_dotprod_with_pragma:
      P1 = R2 ;
      P2 = R0 ;
      CC = R0 < = 0;
      R0 = 0;
      IF CC JUMP ._P1L8 ;
.align 8;
      I0 = R1 ;
      A0 = 0 || R0 = W[P1++] (X) || NOP;
      R1.L = W[I0++];
      LSETUP (._P1L5 , ._P1L6-8) LC0 = P2;
._P1L5:
      A0 += R0.L*R1.L (IS) || R0 = W[P1++] (X) ||
      R1.L = W[I0++];
._P1L6:
      R0 = A0.w;
      R0 = R0.L (X);
._P1L8:
      RTS;

Компилятор сгенерировал цикл, в котором те же самые инструкции в теле цикла, но здесь компилятор выполняет его n раз, а не n-1 раз. Это означает, что n-тая итерация цикла прочитает x[n] и y[n], что не происходит для dotprod_normal(). Значения, полученные этими чтениями, отбрасываются, поскольку они не нужны, но компилятор получает преимущество, потому что теперь не нужно вычислять n-1 и определять, можно ли выполнять цикл.

Дополнительные чтения можно делать только тогда, ни neither x[], ни y[] yt не попадают на конец допустимой области памяти. Если Вы используете прагму extra_loop_loads, то Вы должны гарантировать, что диапазоны памяти в цикле непрерывны с допустимыми областями памяти, так что если будет дополнительная итерация, то чтение произойдет по допустимому адресу.

Обратите внимание, что когда прагма no_vectorization опущена, компилятор попытается выполнить векторизированный цикл. Прагма extra_loop_loads не влияет на векторизированную версию, поскольку компилятор все равно выполнит по условию выполнение одной заключительной итерации для случаев, когда счетчик цикла нечетное число.

Прагма extra_loop_loads не дает эффекта в случаях:

• Загрузки из volatile адресов; это не дает возможности спекулятивного доступа.
• Загрузки из банков памяти, для которых стоимость доступа на чтение больше одного цикла.
• Компилятор может определить количество итераций, требуемых для цикла, либо через распространение константы, либо через прагмы loop_count. В таких случаях компилятор не нуждается в выполнении спекулятивных чтений.
• Соотношение скорость / место под код не дает компилятору разворачивать тело цикла, поскольку это приводит к увеличению размера кода.

Также см. опцию -extra-loop-loads switch в руководстве [1].

Прагма loop_count появляется сразу перед циклом, который она описывает. Это утверждает, что цикл выполняется как минимум min раз, не больше чем max раз, и что количество итераций нацело делится на modulo. Эта информация позволяет компилятору не вставлять предохранительные проверки, чтобы решить, нужно ли полностью развернуть цикл или нужен ли дополнительный код, чтобы поддерживать нечетное количество итераций. Любой из параметров этой прагмы, который не известен, может быть оставлен пустым. Пример:

int i;
#pragma loop_count(24, 48, 8)
for (i=0; i < n; i++)
   ...

Прагма loop_unroll может использоваться только перед циклом for, while или do.. while. Прагма принимает один положительный целочисленный аргумент N, и инструктирует компилятор развернуть цикл N раз до следующей трансформации кода.

В большинстве случаев эффект от

#pragma loop_unroll N
for ( init statements; condition; increment code) {
   loop_body
}

эквивалентен трансформации цикла в следующий код:

for ( init statements; condition; increment code) {
   loop_body /* копия 1 */
   increment_code
   if (!condition)
      break;
   loop_body /* копия 2 */
   increment_code
   if (!condition)
      break;
   ...
   loop_body /* копия N-1 */
   increment_code
   if (!condition)
      break;
   loop_body /* копия N */
}

Наподобие эффект от

#pragma loop_unroll N
while ( condition ) {
   loop_body
}

эквивалентен:

while ( condition ) {
   loop_body /* копия 1 */
   if (!condition)
      break;
   loop_body /* копия 2 */
   if (!condition)
      break;
   ...
   loop_body /* копия N-1 */
   if (!condition)
      break;
   loop_body /* копия N */
}

И эффект от

#pragma loop_unroll N
do {
   loop_body
} while ( condition )

эквивалентен преобразованию цикла в код:

do {
   loop_body /* копия 1 */
   if (!condition)
      break;
   loop_body /* копия 2 */
   if (!condition)
      break;
   ...
   loop_body /* копия N-1 */
   if (!condition)
      break;
   loop_body /* копия N */
} while ( condition )

Используйте прагму no_alias для информирования компилятора, что следующий цикл не имеет конфликтующих загрузок или сохранений. Когда компилятор находит память, которая потенциально относится к тем же самым ячейкам через разные указатели (известные как "алиасы"), то компилятор ограничен в том, как он может переорганизовать или векторизировать цикл, потому что все доступы предыдущих итераций должны быть завершены до того, как компилятор может принять меры для запуска следующей итерации. Пример:

void vadd(int *a, int *b, int *out, int n) {
   int i;
#pragma no_alias
   for (i=0; i < n; i++)
      out[i] = a[i] + b[i];
}

Прагма no_alias появилась сразу перед циклом, который она описывает. Эта прагма утверждает, что в следующем цикле нет операций чтения или сохранения, конфликтующих друг с другом. Другими словами, нет чтения или сохранения в любой итерации цикла с тем же адресом, что и любая другая загрузка или сохранение в этой или любой другой итерации цикла. В приведенном примере если указатели a и b указывают на две не перекрывающиеся области памяти, то не будет загрузок из b, использующих тот же адрес, что и любое сохранение в a. Таким образом, a никогда не будет алиасом для b.

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

Прагма no_vectorization отключает всю векторизацию для цикла, к которому эта прагма применена.

Прагма vector_for оповещает компилятор, что можно безопасно выполнить параллельно две итерации цикла. Прагма vector_for не принуждает компилятор векторизировать цикл. Оптимизатор проверяет разные свойства цикла и не делает векторизацию, если код не безопасный, или нельзя вычислить, что допустимы разные свойства, необходимые для векторизации.

Строго говоря, прагма просто запрещает проверку переносимых по циклу зависимостей.

void copy(short *a, short *b) {
   int i;
#pragma vector_for
   for (i=0; i < 100; i++)
      a[i] = b[i];
}

В случаях, когда векторизация невозможна (например, если массив a выровнен по границе слова, а массив b нет), то информация в утверждении vector_for все еще может найти хорошее применение для помощи других оптимизаций.

[Директивы #pragma для общей оптимизации]

Компилятор поддерживает несколько прагм, которые могут поменять уровень оптимизации в процессе компиляции модуля. Эти прагмы должны быть использованы глобально, немедленно перед определением функции. Прагмы не просто прикладываются немедленно в следующей за ними фунции; действие приложенной прагмы сохраняется до окончания компиляции, или пока действие не будет переназначено одной из следующих прагм optimize_:

#pragma optimize_off. Эта прагма отключает оптимизатор, если он был разрешен. Это дает тот же эффект, как и компиляция с не разрешенной оптимизацией.

#pragma optimize_for_space. Эта прагма включает оптимизатор, если он был запрещен, или устанавливает фокус оптимизации на обеспечение приоритета уменьшения размера кода в противовес оптимизации для улучшения быстродействия - в том случае, если эти оптимизации противоречат друг другу.

#pragma optimize_for_speed. Эта прагма включает оптимизатор, если он был запрещен, или устанавливает фокус оптимизации на обеспечение приоритета ускорения работы кода в противовес оптимизации для уменьшения размера кода - в том случае, если эти оптимизации противоречат друг другу.

#pragma optimize_as_cmd_line. Эта прагма сбрасывает настройки оптимизации к тем значениям, которые заданы в командной строке компилятора ccblkfn, когда он запускается для компиляции кода.

В следующих примерах показано использование прагм optimize_.

#pragma optimize_off
void non_op() { /* не оптимизированный код */ }
 
#pragma optimize_for_space
void op_for_si() { /* этот код будет оптимизирован по размеру */ }
 
#pragma optimize_for_speed
void op_for_sp() { /* этот код будет оптимизирован на скорость */ }
/* Последующие функции все будут оптимизированы на скорость */

[Ссылки]

1VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors site:analog.com.
2VisualDSP: полезные директивы #pragma.

 

Добавить комментарий


Защитный код
Обновить

Top of Page