В этом документе предоставлены некоторые полезные методики для получения наиболее эффективного и быстрого кода для процессора семейства Blackfin®, при использовании компилятора языка C/C++ из среды разработки VisualDSP++™ (перевод документа EE-149 [1] компании Analog Devices).
Используйте оптимизатор кода. Есть значительные отличия в эффективности кода C, когда он компилируется с оптимизацией или без неё. В некоторых случаях оптимизированный код может работать в 10..12 раз быстрее. Всегда должна быть сделана попытка оптимизации перед выполнением проверки скорости работы кода или перед перемещением кода в готовый продукт. Обратите внимание, что настройка проекта по умолчанию рассчитана на не оптимизированную компиляцию, потому что не оптимизированный вариант кода больше подходит для программистов в поиске ошибок и устранении проблем при начальных стадиях кодирования.
Прим. переводчика: переключение проекта из настроек Debug в настройки Release и обратно очень помогает при поиске багов и не оптимально написанных кусков кода. Хорошо написанный код должен быть работоспособен в обоих вариантах компиляции, и в Debug, и в Release. Если вдруг обнаружилось, что одном из вариантов программа не работает, то наверняка есть какой-то нехороший косяк.
Оптимизатор компилятора языка C процессора Blackfin разработан для генерации эффективного по быстродействию двоичного кода, который написан в простом, линейном стиле. Базовая стратегия при алгоритмической оптимизации - представить алгоритм таким способом, который даст оптимизатору отличную видимость операндов и данных программы, и следовательно даст большую свободу безопасно манипулировать кодом (KISS. Короче говоря, сильно умничать в написании программы не стоит). Обратите внимание, что будущие релизы будут улучшать оптимизатор, представление алгоритма в более простом виде даст больше выгоды при внедрении таких улучшений.
Используйте статистический профайлер. Настройка кода начинается с понимания, какие места в программе являются наиболее узким местом, т. е. на какие вычисления процессор затрачивает больше всего времени, и какие места в коде наиболее критичны к времени выполнения. Статистический профайлинг, предоставленный средой разработки VisualDSP++ являются превосходным инструментом для нахождения этих горячих мест.
Если Вы не знакомы с приложением, то скомпилируйте его с включенным выводом диагностики, и в не оптимизированном варианте. Это даст Вам результаты, которые максимально соответствуют исходному коду на языке C. Вы получите более точное представление своего приложения по сравнению с полностью оптимизированным приложением, и получите статистические данные, которые относятся непосредственно к коду ассемблера. Единственная проблема может быть в соотношении строк ассемблера и строк в оригинальном исходном коде. Не удаляйте имена функций из объектного кода при линковке. Если будут имена функций, то Вы можете прокрутить окно кода ассемблера в отладчике и по именам функций найти горячие места в коде. В очень сложном коде Вы можете точно найти строки исходного кода путем подсчета циклов – если они не развернуты. В .s файлах есть номера строк, их можно использовать. Имейте в виду, что оптимизатор может перемещать код.
Типы данных. Для эффективной работы программы важно правильно выбрать используемый тип данных для переменных.
Таблица 1. Типы данных с фиксированной точкой, традиционная (Native) арифметика.
char |
8-битное целое со знаком |
unsigned char |
8-битное целое без знака |
short |
16-битное целое со знаком |
unsigned short |
16-битное целое без знака |
int |
32-битное целое со знаком |
unsigned int |
32-битное целое без знака |
long |
32-битное целое со знаком |
unsigned long |
32-битное целое без знака |
Под традиционной арифметикой подразумевается, что процессор и компилятор напрямую используют аппаратные возможности процессора, и эти типы данных обрабатываются наиболее эффективно. Тип double для процессоров Blackfin эквивалентен типу float. 64-битные значения и типы данных с плавающей точкой не поддерживаются аппаратно процессором Blackfin, поэтому компилятор для их обработки генерирует специальный код, что снижает быстродействие работы программы.
Таблица 2. Типы данных с плавающей точкой, арифметика с эмуляцией операций.
float |
32-битное число с плавающей запятой (floating point) |
double |
32-битное число с плавающей запятой (floating point) |
Дробные типы данных с фиксированной точкой представлены либо как short, либо как int [2]. Операции с этими типами лучше всего осуществлять с помощью встраиваемых функций и макросов (intrinsics), что будет описано в следующих секциях.
Избегайте арифметики с типами float и double. Арифметика с плавающей точкой реализует операции через вызовы библиотечных подпрограмм, и следовательно, такие операции будут намного медленнее целочисленных. Арифметика floating-point в теле цикла не даст оптимизатору применить аппаратный цикл.
Избегайте в теле цикла операций целочисленного деления. Аппаратура процессора не предоставляет прямую поддержку 32-битного целочисленного деления, так что операции деления и операции modulus для переменных int будут многоцикловыми операциями. Компилятор преобразует целочисленное деление в операции сдвига вправо, если ему известно значение делителя.
Если компилятор испытывает затруднения в реализации полной операции деления, то он вызовет для этого библиотечную функцию. В дополнение к выполнению многоцикловых команд функции, это не даст оптимизатору возможность применить аппаратный цикл для любых циклов, в теле которых есть деление. Всегда, когда это возможно, избегайте операций деления и modulus внутри цикла.
Выбор способа доступа к элементам массива. Что лучше - индексирование элементов массива или указатели? Язык C позволяет Вам программировать доступ к данных массивов двумя методами: либо по индексу от инвариантного базового указателя, либо по инкрементируемому указателю. Приведенные ниже 2 версии векторного сложения иллюстрируют эти два стиля доступа:
void va_ind (short a[], short b[], short out[], int n)
{
int i;
for (i = 0; i < n; ++i)
out[i] = a[i] + b[i];
}
Листинг 1. Доступ к массиву по индексу.
void va_ptr ( short a[], short b[], short out[], int n)
{
int i;
short *pout = out, *pa = a, *pb = b;
for (i = 0; i < n; ++i)
*pout++ = *pa++ + *pb++;
}
Листинг 2. Доступ к массиву по указателю.
Можно было бы подумать, что оба стиля никак не должны отличаться по сгенерированному коду, но иногда отличия все же есть. Часто одна из версий алгоритма дает лучшую оптимизацию кода, чем другая, но не всегда лучше один и тот же стиль. На генерацию кода влияет окружающий код, от которого зависит, почему появляются отличия. Стиль с указателем вводит дополнительные переменные, которые конкурируют с окружающим кодом при анализе оптимизатором. С другой стороны, доступ через индексирование массива может быть преобразован компилятором в доступ через указатели, и так что компилятор иногда не делает то, что Вы выполнили вручную.
Лучшая стратегия - начать с доступа через индекс массива. Если результат получился неудовлетворительным, попробуйте использовать указатели. За исключением особо важных циклов всегда используйте индексированный стиль, поскольку он проще для понимания.
Используйте опцию -ipa компилятора. Чтобы обеспечить наилучшую производительность, оптимизатор часто нуждается в информации, которую можно получить только рассматривая окружающий оптимизируемую функцию код. В частности, это поможет узнать выравнивание, значение параметров указателя и значение границ цикла. Ключ командной строки -ipa разрешает межпроцедурный анализ (inter-procedural analysis, IPA), который делает эту информацию доступной. В среде разработки VisualDSP это можно включить галочкой "Interprocedural optimization" раздела Compile диалога свойств проекта (меню Project -> Project Options).
Когда используется эта опция, то компилятор будет запущен повторно на фазе линковки, чтобы перекомпилировать программу и использованием дополнительной информации, полученной на предыдущем этапе компиляции.
Примечание: из-за того, что это работает только во время линковки, то эффекты от применения опции -ipa не будут видны при компиляции с опцией -S (опцию -S используют, чтобы получить ассемблерный листинг программы). Чтобы просмотреть файл ассемблера, добавьте опцию -save-temps в поле ввода дополнительных опций (Additional Options) в диалоге настройки свойств проекта, раздел Compile, и просмотрите файл .s, который будет сгенерирован при сборке.
Большая часть следующих советов подразумевает использование ключа -ipa в командной строке компилятора.
Инициализируйте константы статически. Межпроцедурный анализ (IPA) также идентифицирует переменные, у которых есть только одно значение, и заменяет их на константы, что может дать улучшение оптимизации. Чтобы это произошло, переменная должна иметь одно значение на все время жизни программы.
#include < stdio.h >
static int val = 3; // единственная инициализация
void init() {
}
void func() {
printf("val %d",val);
}
int main() {
init();
func();
}
Листинг 3: Оптимальная инициализация (IPA знает, что переменная val равна 3).
Если переменная статически инициализирована в 0, как по умолчанию делается со всеми глобальными переменными, и потом ей назначается какое-то иное значение в другом месте программы, то анализ увидит 2 значения и не будет считать переменную константой.
#include < stdio.h >
static int val; // переменная инициализирована нулем
void init() {
val = 3; // повторная инициализация
}
void func() {
printf("val %d",val);
}
int main() {
init();
func();
}
Листинг 4: не оптимально (IPA не может увидеть, что val константа).
Выравнивайте свои данные на размер слова. Чтобы использовать аппаратуру наилучшим образом, для неё нужно эффективно предоставлять данные. Во многих алгоритмах баланс доступа данных и вычислений с полностью задействованной аппаратурой будет тогда, когда данные перемещаются из памяти в регистры процессора и обратно 32-битными порциями.
Хотя архитектура Blackfin поддерживает и побайтную адресацию, аппаратура требует обращения к памяти с традиционным выравниванием. Таким образом, 16-битное обращение должно быть по четным адресам ячеек памяти (выравнивание адреса на размер полуслова), и для 32-битных обращений адрес должен нацело делиться на 4 (выравнивание адреса на слово). Таким образом, чтобы получился максимально эффективный код, Вам нужно гарантировать, что все данные выровнены в памяти на размер слова (4 байта, адрес нацело делится на 4).
Компилятор помогает добиться выравнивания данных массива. Окна стека сохраняются выровненными на слово. Массивы верхнего уровня размещаются по начальному адресу, выровненному на слово, независимо от типа находящихся в массиве данных.
Если Вы пишете программы, которые передают только адрес первого элемента в массиве как параметр, и есть циклы, которые обрабатывают входные массивы по одному элементу за итерацию, начните с элемента 0 массива, тогда IPA будет в состоянии выяснить, что выравнивание подходит для 32-битного доступа.
Там, где внутренний цикл обрабатывает построчно многомерный массив, удостоверьтесь, что каждая строка начинается на адресе, выровненном на слово. Это можно сделать, добавляя в массив или структуру пустые элементы.
[Советы по организации циклов]
В приложении A дан обзор того, как оптимизатор преобразует цикл для генерации высокоэффективного кода. Это описано техникой "разворачивание цикла" (loop unrolling).
Не разворачивайте циклы самостоятельно (KISS, не надо умничать!). Разворот цикла не только делает программу трудной для чтения, но и не дает компилятору применить оптимизацию. Компилятор должен развернуть цикл сам, чтобы автоматически использовать широкие загрузки и оба аккумулятора.
void va1 (short a[], short b[], short c[], int n)
{
int i;
for (i = 0; i < n; ++i)
{
c[i] = b[i] + a[i];
}
}
Листинг 5: оптимально (компилятор развернет и будет использовать оба блока вычисления).
void va2 (short a[], short b[], short c[], int n)
{
short xa, xb, xc, ya, yb, yc;
int i;
for (i = 0; i < n; i+=2)
{
xb = b[i]; yb = b[i+1];
xa = a[i]; ya = a[i+1];
xc = xa + xb; yc = ya + yb;
c[i] = xc; c[i+1] = yc;
}
}
Листинг 6: не оптимально (компилятор оставит 16-битные загрузки).
В этом примере первая версия цикла работает примерно в 3 раза быстрее, чем вторая - в тех случаях, когда IPA может определить, что начальные значения a, b и c выровнены на 32-битные границы адреса, и n кратно двум.
Избегайте зависимостей в теле цикла. Зависимость в цикле (loop-carried dependency) это случай, когда вычисление текущей итерации цикла нельзя осуществить без знания значений, которые вычисляются на предыдущих итерациях. Когда в цикле есть такие зависимости, компилятор не может перекрыть итерации цикла.
Некоторые зависимости получаются от скалярных переменных, которые используются перед тем, как они будут определены в одной итерации.
for (i = 0; i < n; ++i)
x = a[i] - x;
Листинг 7: не оптимально (скалярная зависимость).
Оптимизатор может переупорядочить итерации в присутствии класса скалярных зависимостей, известных как редукции (reduction). Это циклы, которые уменьшают значения вектора к скалярной величине, используя ассоциативный или коммутативный оператор. Ниже приведен самый общий пример умножения с накоплением (multiply and accumulate, MAC).
for (i = 0; i < n; ++i)
x = x + a[i] * b[i];
Листинг 8: оптимально (редукция).
В первом случае скалярная зависимость это операция вычитания; переменная x изменяется таким образом, который даст различные результаты, если итерации будут выполнены не в том порядке. Второй случай, в отличие от первого, где используется оператор сложения, позволяет компилятору выполнить операции в любом порядке, с получением одинакового результата.
Не переворачивайте циклы вручную. Циклы в коде DSP часто "повернуты" программистом вручную, в попытке сделать загрузки (load) и сохранения (store) раньше и будущие итерации выполнить одновременно с текущей итерацией. Эта техника вводит зависимость в цикл (loop-carried dependence), которая не дает компилятору эффективно переорганизовать код. Лучше дать компилятору "нормализованную" версию, и оставить повороты компилятору.
int ss (short *a, short *b, int n)
{
short ta, tb;
int sum = 0;
int i = 0;
ta = a[i]; tb = b[i];
for (i = 1; i < n; i++)
{
sum += ta + tb;
ta = a[i]; tb = b[i];
}
sum += ta + tb;
return sum;
}
Листинг 9: не оптимально (повернуто вручную).
При повороте цикла были добавлены переменные ta и tb, которые ввели зависимость в цикл, что не дает компилятору вычислять итерации параллельно. Оптимизатор может делать ротации такого рода самостоятельно.
int ss (short *a, short *b, int n)
{
short sum = 0;
int i;
for (i = 0; i < n; i++)
{
sum += a[i] + b[i];
}
return sum;
}
Листинг 10: оптимальная организация цикла (ротация делается компилятором).
Избегайте записей в массив в цикле. Другие зависимости могут получиться при записи в элементы массива. В следующем примере цикла оптимизатор не может определить, была ли загрузка от чтения значения, определенная на предыдущей итерации, или она будет перезаписана в последующей итерации.
for (i = 0; i < n; ++i)
a[i] = b[i] * a[c[i]];
Листинг 11: не оптимально (зависимость из-за массива).
for (i = 0; i < n; ++i)
a[i+4] = b[i] * a[i];
Листинг 12: Оптимально (индукционные переменные).
Оптимизатор может разрешить схемы доступа, где адреса - выражения, которые варьируются на фиксированное количество при каждой итерации. Эта схема известна как "индукционные переменные" (induction variables).
Избегайте псевдонимов (aliases). Цикл может выгладить как не содержащий зависимостей, но a и b оба параметры, и, хотя они декларированы с квадратными скобками [ ], они фактически указатели, которые могут указывать на тот же массив. Когда к тем же самым данным можно получить доступ через 2 указателя, мы говорим, что они могут являться псевдонимами друг друга.
void fn (short a[], short b[], int n)
{
for (i = 0; i < n; ++i)
a[i] = b[i];
}
Листинг 13: не оптимально (потенциальный алиасинг).
Если применен ключ командной строки -ipa, компилятор может просмотреть места вызовов fn, и возможно определить, указывают ли они когда-нибудь на тот же самый массив.
Даже с опцией -ipa довольно просто создать очевидные псевдонимы. IPA работает, связывая указатели с набором переменных, к которым они могут обратиться в некоторой точке программы. Для упрощения анализа не учитывается управление потоком вычислений, и если набор для двух указателей найден пересекающимся, то оба указателя подразумеваются как указывающие на объединение (union) двух наборов.
Если вышеприведенная функция fn была вызвана в 2 местах кода с глобальными массивами в качестве аргументов, то IPA покажет следующие результаты для разных случаев:
Случай 1:
fn(glob1, glob2, N); наборы не пересекаются: a и b fn(glob1, glob2, N); нет псевдонимов (оптимально)
Случай 2:
fn(glob1, glob2, N); наборы не пересекаются: a и b fn(glob3, glob4, N); нет псевдонимов (оптимально)
Случай 3:
fn(glob1, glob2, N); наборы пересекаются: a и b fn(glob3, glob1, N); могут быть взаимные ссылки (алиасы, не оптимально)
Третий случай показывает, что IPA рассматривает объединение для всех вызовов сразу, вместо рассмотрения каждого вызова индивидуально, когда определяет, есть ли риск алиасинга. Если бы каждый вызов был рассмотрен индивидуально, то IPA должен учесть управление потоком, и количество перестановок сделало бы время компиляции недопустимо долгим.
Особенно тщательно проектируйте внутренний цикл. Оптимизатор фокусируется на внутренних циклах, потому что программы больше всего тратят процессорного времени на прохождение именно внутренних циклов. Считается хорошим компромиссом для оптимизации замедлить вычисления до и после цикла, если это поможет выполнить тело цикла быстрее. Так что удостоверьтесь что Ваш алгоритм проводит большую часть своего времени во внутреннем цикле. Иначе возможно, что фактически после оптимизации он будет работать медленнее.
Полезна так называемая техника переключения цикла (loop switching). Если у Вас есть вложенные циклы, где внешний цикл работает много раз, и внутренний цикл работает меньшее количество раз, то можно переписать циклы так, чтобы внешний цикл делал меньше итераций.
Избегайте условных ветвлений внутри циклов. Если цикл содержит код с проверками условий, то могут быть слишком большие потери процессорного времени, если решение будет часто давать переходы, не соответствующие прогнозу компилятора. В некоторых случаях компилятор сможет преобразовать конструкции if-else и ?: в условные копирования данных. В других случаях он сможет полностью вытащить вычисление выражения из тела цикла. Однако для критичных циклов должен быть написан линейный код, без ветвлений.
Делайте циклы короче. Чтобы компилятор работал максимально эффективно, циклы должны быть как можно меньше. Большие по размеру тела цикла обычно намного сложнее для оптимизации. Дополнительно может потребоваться сохранять данные из регистров в память. Это приведет к снижению плотности кода и скорости выполнения.
Не вставляйте вызовы функций в тело циклов. Компилятор не генерирует аппаратных циклов, если в цикле содержится вызов функции, потому что слишком затратно сохранять и восстанавливать контекст аппаратного цикла при вызове функции. В дополнение к очевидным вызовам функций, таких как printf(), генерация аппаратного цикла будет запрещена следующими операциями: modulus и целочисленное деление, арифметика с плавающей точкой, и преобразование между целочисленными данными и данными с плавающей точкой. Эти операции могут неявно потребовать вызовов библиотечных функций.
Используйте int для управляющих переменных цикла и для индексов массива. Всегда лучше использовать для этих случаев переменные целого типа int, чем переменные типа short. Стандарт C утверждает, что short должен быть расширен до размера int перед выполнением вычисления, и затем обратно обрезан до размера short.
Часто компилятор в состоянии работать со счетчиками цикла short, и все еще определять возможность использовать аппаратные циклы (zero-overhead loops) и индукционные переменные (induction variables). Однако это усложняет жизнь компилятору, и иногда может привести к менее оптимизированному коду.
Прагмы для цикла: как избежать векторизации. Пример:
void copy (short *a, short *b)
{
int i;
for (i=0; i < 100; i++)
a[i] = b[i];
}
Листинг 14: не оптимально (без использования pragma).
Если мы дважды вызовем функцию copy из листинга 14, скажем сначала copy(x, y) и затем copy(y, z), то IPA не сможет сказать, что a никогда не будет алиасом b, как это описано выше. Таким образом, цикл содержит зависимость, и не может быть векторизирован. В этом случае советуют использовать прагму 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];
}
Листинг 15: оптимально (используется pragma).
Обратите внимание, что эта прагма не принуждает компилятор делать векторизацию цикла; оптимизатор проверит различные свойства цикла и не будет делать его распараллеливание, если посчитает его небезопасным, или если если не сможет вывести различные параметры, необходимые для преобразования векторизации. Прагма гарантирует компилятору, что тут не могут быть зависимости в цикле, однако другие свойства цикла все же могут не дать применить векторизацию.
В случаях, когда векторизация невозможна (например, если массив a выровнен на размер слова, но массив b не выровнен), то информация, которую дает установка vector_for, все еще может хорошо помочь для других оптимизаций.
[Использование данных]
Константы. По умолчанию компилятор предполагает, что не будут изменяться те данные, на которые ссылается указатель на данные const. Таким образом есть другой способ, которые скажет о двух массивах, что они не перекрываются - использовать ключевое слово const.
Пример в листинге 16 будет иметь эффект, подобный прагме vector_for. Фактически реализация с const лучше, поскольку она также позволит оптимизатору, после векторизации, повернуть цикл, что требует не только знаний о том, что нет зависимости одной прокрутки цикла от другой, но и также о том, что итерации выполняются в будущем обособленно.
void copy (short *a, const short *b)
{
int i;
for (i=0; i < 100; i++)
a[i] = b[i];
}
Листинг 16: использование ключевого слова const.
На языке C допустимо (хотя и считается плохой практикой в программировании) использовать приведения типов (cast), чтобы можно было менять данные, на которые указывает указатель на данные const. Этого нужно избегать, потому что, по умолчанию, компилятор будет генерировать код, который подразумевает, что данные const не меняются. Однако, если у Вас программа модифицирует данные const, используя указатель, Вы можете генерировать корректный код, используя флаг времени компиляции -const-read-write.
Дробные данные (Fractional Data). Дробями, представленными как 16-битные или 32-битные целые числа, можно манипулировать двумя способами. Рекомендуемый способ, который дает Вам больше контроля над данными - использовать так называемые встроенные объекты (intrinsics). Давайте рассмотрим дробное скалярное произведение. Его код можно записать так:
int sp (short *a, short *b)
{
int i;
int sum=0;
for (i=0; i < 100; i++)
{
sum += ((a[i]*b[i]) >> 15);
}
return sum;
}
Листинг 17: не оптимально (используются сдвиги).
Однако этот код доставит некоторые проблемы оптимизатору. Обычно сгенерированный здесь код был бы умножением, за которым идет сдвиг, сопровождаемый накоплением. Однако у процессора Blackfin есть инструкция MAC (multiply accumulate), которая делает в одном цикле умножение дробного числа с накоплением. Кроме того, процессор может выполнить 2 такие инструкции параллельно.
Компилятор распознает эту идиому и подтвердит, что в мире DSP-программирование предпочтение отдается арифметике с насыщением (saturating arithmetic). Умножение / сдвиг заменяется умножением дробного числа с насыщением. Это преобразование можно запретить использованием ключа командной строки -no_int_to_fract в том случае, если насыщение не требуется.
Однако это был простой случай. В более сложных ситуациях, где возможно что в будущем умножение отделено от сдвига, компилятор может не обнаружить возможности использовать дробное умножение. Так что рекомендуемый стиль программирования - использовать intrinsics. В следующем примере add_fr1x32() и mult_fr1x32 используются соответственно для добавления и для умножения дробных 32-битных данных.
#include < fract.h >
fract32 sp (fract16 *a, fract16 *b)
{
int i;
fract32 sum=0;
for (i=0; i < 100; i++)
{
sum = add_fr1x32(sum,
mult_fr1x32(a[i],b[i]));
}
return sum;
}
Листинг 18: оптимально (используются встроенные функции, intrinsics).
Полный список дробных операций можно найти в руководстве по компилятору процессора (Blackfin processor C/C++ Compiler manual), совместно с описанием других доступных intrinsic. Функции intrinsic предоставляют операции, которые главным образом работают с 16- или 32-битными величинами; компилятор будет распознавать ситуации, когда цикл может быть векторизован и будет в таких обстоятельствах генерировать двойные 16-битные операции (парные операции, распараллеливание). Точно так же, как лучше оставить поворот цикла компилятору, intrinsic-функции также оставляют компилятору генерировать парные операции.
Помещайте массивы в разные секции памяти. Есть возможность поместить массивы в разные секции памяти. Процессор Blackfin может поддержать 2 операции над памятью в одной инструкции. Однако это будет выполнено в одном цикле если 2 адреса соответствуют разным блокам памяти; если доступ осуществляется в тот же самый блок, то будет вставлена задержка. Возьмем в качестве примера скалярное произведение (как было показано в предыдущей секции).
Поскольку данные загружаются из массивов a и b по каждому циклу, может быть полезно убедиться, что эти массивы принадлежат разным физическим блокам памяти. Например, рассмотрим определение 2 банков в разделе MEMORY настроек .LDF-файла.
MEMORY {
BANK_A1 {
TYPE(RAM) WIDTH(8)
START(0xFF900000) END(0xFF900FFF)
}
BANK_A2 {
TYPE(RAM) WIDTH(8)
START(0xFF901000) END(0xFF901FFF)
}
}
Листинг 19: карта оперативной памяти из файла LDF.
Затем сконфигурируйте раздел SECTIONS, чтобы указать линкеру разместить секции данных по определенным банкам памяти, как это показано ниже.
SECTIONS {
bank_a1 {
INPUT_SECTION_ALIGN(2)
INPUT_SECTIONS( $OBJECTS(bank_a1))
} >BANK_A1
bank_a2 {
INPUT_SECTION_ALIGN(2)
INPUT_SECTIONS( $OBJECTS(bank_a2))
} >BANK_A2
}
Листинг 20: назначение секции в файле LDF.
В исходном коде на языке C/C++ секции определяются конструкцией section("section_name"), которая предшествует декларации буфера.
section("bank_a1") short a[100];
section("bank_a2") short b[100];
Листинг 21: Назначение секций в исходном коде C.
Обратите внимание, что явное размещение данных в секциях можно осуществить только для глобальных данных. За подробностями пожалуйста обратитесь к руководству пользователя линкера и утилит VisualDSP (VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors).
[Приложение A: как работает оптимизатор]
Мы будем использовать следующий цикл скалярного умножения дробных чисел, чтобы показать работу оптимизатора.
#include < fract.h >
fract32 sp(fract16 *a, fract16 *b) {
int i;
fract32 sum=0;
for (i=0; i < 100; i++) {
sum = add_fr1x32(sum,
mult_fr1x32(a[i],b[i]));
}
return sum;
}
Листинг 22: Скалярное умножение дробных чисел.
После генерации кода и стандартной скалярной оптимизации получится примерно такой двоичный код цикла:
P2 = 100;
LSETUP(.P1L3, .P1L4 – 2) LC0 = P2;
.P1L3:
R0 = W[P0++] (X);
R2 = W[P1++] (X);
A0 += R0.L * R2.L;
.P1L4:
R0 = A0.w;
Листинг 23: код на выходе компилятора.
Проверка на выход из цикла была перемещена в конец цикла, и работа счетчика цикла переписана так, что он считает вниз до нуля. Сумма накапливается в аккумуляторе A0. Регистры указателей P0 и P1 содержат указатели, которые инициализированы параметрами A и B соответственно, и они инкрементируются на каждой итерации цикла. Чтобы использовать 32-битный доступ к памяти, оптимизатор развернул цикл для параллельного запуска двух итераций. Половинки суммы теперь аккумулируются в A0 и A1, и они должны быть сложены друг с другом на выходе после завершения цикла, чтобы получить конечный результат. Чтобы использовать загрузки слова, компилятор знает о том, что начальные значения в P0 и P1 нацело делятся на 4 (адреса выровнены на размер слова). Также обратите внимание, что компилятор знает о том, что оригинальный цикл в исходном коде выполняется четное количество раз, так что выполненная по условию нечетная итерация должна быть вставлена вне тела цикла.
P2 = 50;
A1 = A0 = 0;
LSETUP(.P1L3, .P1L4 – 4) LC0 = P2;
.P1L3:
R0 = [P0++];
R2 = [P1++];
A1+=R0.H*R2.H, A0+=R0.L*R2.L;
.P1L4:
R0 = (A0+=A1);
Листинг 24: Дополнительная нечетная итерация.
И наконец оптимизатор поворачивает цикл, разворачивая и перекрывая итерации, чтобы получить максимальное использование функциональных блоков. В заключение будет сгенерирован следующий код:
A1=A0=0 || R0 = [P0++] || NOP;
R2 = [I1++];P2 = 49;
LSETUP(.P1L3,.P1L4-8) LC0 = P2;
.P1L3:
A1+=R0.H*R2.H, A0+=R0.L*R2.L || R0 =[P0++] || R2 = [I1++];
.P1L4:
A1+=R0.H*R2.H, A0+=R0.L*R2.L;
R0 = (A0+=A1);
Листинг 25: Код на выходе оптимизатора.
[Приложение B: ключи командной строки компилятора]
Ключи командной строки компилятора, касающиеся оптимизации, перечислены в таблице 3.
Таблица 3: Опции командной строки компилятора, относящиеся к оптимизации.
-O |
Оптимизация по скорости |
-Os |
Оптимизация по размеру |
-Ox |
Значения переменных типа short остаются в 16-битном диапазоне |
-Ofp |
Изменить смещения указателя фрейма FP так, чтобы использовать инструкции короче. |
-ipa |
Выполнять межпооцедурный анализ и соответствующую оптимизацию |
Примечание: полный список поддерживаемых опций компилятора см. в [3], или обратитесь к руководству пользователя линкера и утилит VisualDSP (VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors).
[Ссылки]
1. EE-149: Tuning C Source Code for the Blackfin® Processor site:analog.com. 2. Поддержка традиционных типов с фиксированной точкой в VisualDSP++. 3. Опции командной строки компилятора Blackfin. |