Научная статья на тему 'Эффективное программирование с учетом архитектурных особенностей цифровых сигнальных процессоров'

Эффективное программирование с учетом архитектурных особенностей цифровых сигнальных процессоров Текст научной статьи по специальности «Компьютерные и информационные науки»

CC BY
254
77
i Надоели баннеры? Вы всегда можете отключить рекламу.

Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Сотников Александр, Катц Дэвид, Джентайл Рик, Лукасек Томаш

Современные цифровые сигнальные процессоры (ЦСП) обладают столь привлекательным сочетанием производительности, рассеиваемой мощности, набора периферийных узлов и цены, что многие разработчики склоняются к их применению вместо процессоров, на которых они традиционно создавали свои системы. Одно из потенциальных препятствий на пути перехода к ЦСП — это большое количество алгоритмов на языках С/C++, написанных разработчиками для своих приложений. Естественно, при этом у них возникает желание перенести существующее программное обеспечение (ПО), написанное на высокоуровневых языках, на платформу ЦСП, что позволит при правильном использовании архитектурных особенностей процессора обеспечить уровень производительности, недостижимый на платформах, с которыми они работали прежде. Более того, они предпочли бы использовать знакомую, интуитивно понятную среду разработки, имея, в то же время, возможность реализовывать отдельные части программы на языке ассемблера для повышения производительности. В этой статье обсуждаются методы и стратегии программирования ЦСП в современной среде разработки ПО.

i Надоели баннеры? Вы всегда можете отключить рекламу.
iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.
i Надоели баннеры? Вы всегда можете отключить рекламу.

Текст научной работы на тему «Эффективное программирование с учетом архитектурных особенностей цифровых сигнальных процессоров»

Дэвид КАТЦ (David KATZ) Томаш ЛУКАШЕК (Tomasz LUKASIAK) Рик ДЖЕНТАЙЛ (Rick GENTILE) Перевод: Александр СОТНИКОВ

Эффективное программирование

с учетом архитектурных особенностей цифровых сигнальных процессоров

Современные цифровые сигнальные процессоры (ЦСП) обладают столь привлекательным сочетанием производительности, рассеиваемой мощности, набора периферийных узлов и цены, что многие разработчики склоняются к их применению вместо процессоров, на которых они традиционно создавали свои системы. Одно из потенциальных препятствий на пути перехода к ЦСП — это большое количество алгоритмов на языках С/C++, написанных разработчиками для своих приложений. Естественно, при этом у них возникает желание перенести существующее программное обеспечение (ПО), написанное на высокоуровневых языках, на платформу ЦСП, что позволит при правильном использовании архитектурных особенностей процессора обеспечить уровень производительности, недостижимый на платформах, с которыми они работали прежде. Более того, они предпочли бы использовать знакомую, интуитивно понятную среду разработки, имея, в то же время, возможность реализовывать отдельные части программы на языке ассемблера для повышения производительности. В этой статье обсуждаются методы и стратегии программирования ЦСП в современной среде разработки ПО.

Ассемблер и высокоуровневые языки программирования

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

Преимущества С/С++ заключаются в модульности, возможности переноса на другие платформы и простоте повторного использования кода. Большинство разработчиков ПО для встраиваемых систем имеют опыт

программирования хотя бы на одном из этих высокоуровневых языков. Кроме того, существует огромная база доступного программного кода, который можно достаточно легко портировать с микроконтроллеров и ЦСП предыдущих поколений на современные ЦСП. Также, как правило, желательно, чтобы команда разработчиков была разбита на несколько групп, занимающихся написанием отдельных системных модулей. Высокоуровневые языки программирования позволяют таким группам заниматься созданием ПО без привязки к конкретной аппаратной базе.

Традиционные языки ассемблера долгое время не пользовались особой популярностью из-за их «таинственного» синтаксиса и странных аббревиатур. Однако в современных архитектурах с так называемым «алгеб-

раическим синтаксисом» ассемблера эти факторы уже не столь явно выражены. В таблице приведены примеры записей типичных команд ЦСП в традиционном стиле и в алгебраическом формате. Очевидно, что вторая форма записи гораздо проще для понимания. В приведенных примерах г — это регистры данных, а р — регистры указателей.

Другая причина, по которой на языке ассемблера достаточно сложно программировать, заключается в том, что операции с данными осуществляются в нем на уровне реальных регистров, вычислительных блоков и блоков памяти ЦСП. В языках С/С++ манипуляции данными обычно выполняются на гораздо более высоком уровне абстракции путем использования переменных и вызовов функций/процедур, что делает программу более понятной.

Современные компиляторы С/С++ достаточно мощны, и многие из них могут весьма эффективно преобразовывать программы, написанные на высокоуровневом языке программирования, в код на языке ассемблера. Зачастую лучше всего просто предоставить всю работу оптимизатору компилятора. Однако факт остается фактом: производительность компилятора подстроена под специфический набор функций, которые его разработчики

Таблица. Сравнение традиционного синтаксиса ассемблера с современным алгебраическим синтаксисом

Тип операции Традиционный синтаксис ассемблера Алгебраический синтаксис ассемблера

Перемещение содержимого регистра mov r7, r0 r7 = r0

Сложение add r0, r1, r2 r0 = r1 + r2

Вычитание sub r3,r3,r1 r3 = r3 - r1

Загрузка регистра из памяти lwr5,p3 r5 = [p3]

Сохранение содержимого регистра в память sw r1, p0 [p0] = r1

Условный переход к метке _equal при равенстве входных операндов (регистров), в противном случае — переход к метке not equal beq r5, r6, _equal bne r5, r6, _not_equal cc = r5 ==r6 if cc jump _equal if !ccjump not equal

Загрузка регистра из памяти с инкрементированием регистра указателя lw r3, p5 addi p5, p5, 1 r3 = [p5++]

Адрес

О

4

8

С

10

14

18

1C 20

24

28

> Базовый адрес и адрес первого элемента = 0x0

> Индексный регистр Ю указывает на адрес 0x0 »Длина буфера I- = 44

(11 элементов данных * 4 байта/элемент)

»Значение регистра модификации МО = 16 (4 элемента * 4 байта/элемент)

0x00000001

0x00000002

0x00000003

0x00000004

0x00000005

0x00000006

0x00000007

0x00000008

0x00000009

ОхОООООООА

0x0000000В

4-е обращение

5-е обращение

Пример:

R0 = [І0++М0];

R1 = [І0++М0]; R2 = [І0++М0]; R3 = [І0++М0]; R4 = [І0++М0];

// R0=1f после выполнения Ю // указывает на 0x10 // R1=5, после выполнения Ю // указывает на 0x20 // R2=9, после выполнения Ю // указывает на 0x04 // R3=2, после выполнения Ю // указывает на 0x14 // R4=6, после выполнения Ю // указывает на 0x24

Рис. 1. Пример циклической буферизации

Младшие разряды адреса Входной буфер Выходной буфер Младшие разряды адреса

000 0x00000000 0x00000000 000

001 0x00000001 0x00000004 100

010 0x00000002 0x00000002 010

011 0x00000003 0x00000006 110

100 0x00000004 0x00000001 001

101 0x00000005 0x00000005 101

110 0x00000006 0x00000003 011

110 0x00000007 0x00000007 111

Пример: LSETUP(start,end) LC0 = Р0; start: R0 = [Ю] || Ю += МО (BREV); end: [I2++] = R0; //Счетчик цикла Р0 = 8 //Ю указывает на входной буфер и автоматически //инкрементируется в режиме бит-реверсной адресации //12 указывает на буфер, полученный при помощи //бит-реверсной адресации

Рис. 2. Механизм аппаратной бит-реверсной адресации

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

В результате разработчики используют язык ассемблера только при необходимости оптимизировать блоки программы, отвечающие за интенсивную обработку данных. Применение ключей оптимизации компилятора высокоуровневого языка может дать хорошие результаты, однако ничто не сравнится с продуманным, непосредственным управлением вычислениями и перемещением данных в ЦСП. Именно поэтому разработчики зачастую используют комбинацию языков СГС++ и ассемблера. Высокоуровневые языки отлично подходят для задач управления и базовых манипуляций данными, а ассемблер идеален для интенсивных числовых расчетов.

Для эффективного написания программ на языке ассемблера от программиста требуется понимание архитектурных особенностей, которые отличают ЦСП от процессоров, не оптимизированных для супербыстрого «перемалывания» чисел. К ним относятся:

• специализированные режимы адресации;

• аппаратная поддержка циклов;

• кэшируемая память;

• многофункциональные команды;

• конвейер с блокировкой;

• гибкий регистровый файл данных.

Использование перечисленных архитектурных особенностей процессора может привести к значительному увеличению скорости вычислений. Обсудим каждую из них более подробно.

Специализированные режимы адресации

Для того чтобы процессор мог в одном цикле обращаться к нескольким словам данных, необходима полная гибкость в формировании адресов. Так как в ряде распространенных приложений, например, при обработке видеоизображений, требуется работа с 8 разрядными данными, для достижения максимальной эффективности процессор должен, в дополнение к более характерной для ЦСП адресации со смещением по 16 или по 32 бита, поддерживать и побайтовую адресацию. Если обращения к памяти ограничены одним фиксированным шагом смещения, то для маскирования необходимых битов могут понадобиться дополнительные процессорные циклы.

Другой полезный режим адресации — это «циклическая адресация». Это свойство процессор должен поддерживать аппаратно, без издержек на программное управление. Поддержка циклической адресации позволяет программисту настраивать в памяти процессора циклические (кольцевые) буферы и автоматически, без дополнительного программного вмешательства, выполнять адресацию их элементов со смещением указателя.

Генератор адреса поддерживает значения шага по индексу, отличные от единицы, и, что более важно, автоматически выполняет «циклический возврат» (wrap around) к началу буфера, как показано на рис. 1. Без такой возможности автоматического формирования адреса программисту понадобилось бы вручную отслеживать перемещение указателя по

буферу, тратя на это ценные процессорные циклы.

Еще одним важным с точки зрения эффективного выполнения алгоритмов обработки сигналов, таких как БПФ и дискретное косинусное преобразование (DCT), режимом адресации является бит-реверсная адресация (bit reversal). Как следует из названия, при та-

Кэш команд

Вход 1

Вход 2

Вход 3

Вход 4

Кэш данных

Внешняя память большого объема

Заполнение кэш-памяти

обмен в режиме DMA

А________і________К

Внутренняя память: малая емкость, высокое быстродействие

Func А

Func В

Func С

Func D

Func Е

Func F

Mainf)

Таблицы

Данные

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

После активации кэша и настройки контроллера ОМА программист может

сосредоточиться на разработке алгоритма работы ядра.

Высокоскоростные

периферийные

узлы

Внешняя память: большая емкость, меньшее быстродействие

Рис. 3. Оптимизация процесса перемещения данных за счет применения конфигурируемой структуры кэша и памяти

кой адресации порядок следования битов в двоичном адресе изменяется на обратный. То есть младшие разряды меняются местами со старшими разрядами адреса. Подобное упорядочивание данных необходимо для выполнения операции «бабочка» по основанию 2, поэтому бит-реверсная адресация применяется для связи отдельных каскадов БПФ. Организовать вычисление указателя с реверсированием битов можно и программно, однако это будет очень неэффективно. Пример, иллюстрирующий адресацию в режиме с бит-реверсией, показан на рис. 2.

Аппаратная поддержка циклов

Циклы — это одна из основных конструкций алгоритмов обработки данных в системах цифровой обработки сигналов. Существует два ключевых аспекта организации циклов, с помощью которых во многих алгоритмах достигается увеличение быстродействия. Первый из них — это поддержка «аппаратных циклов с нулевыми непроизводительными издержками». Как и в случае с режимами адресации, такие циклы реализуются в процессоре на аппаратном уровне. И, опять же, несмотря на то, что подобная функция может быть реализована программно, связанные с этим непроизводительные издержки скажутся на запасе производительности, необходимом для обработки в режиме реального времени. Поддержка циклов с нулевыми непроизводительными издержками позволяет программистам организовывать циклы, просто указав требуемое количество итераций и границы цикла. При этом процессор будет исполнять команды цикла до достижения счетчиком заданного значения.

Циклы с нулевыми непроизводительными издержками есть в большинстве процессоров, однако действительно существенного прироста производительности при выполнении циклов позволяют достичь «аппаратные буферы циклов». Эти буферы представляют собой подобие кэша для команд, исполняемых в теле цикла. То есть при первом прохождении цикла выполняемые в нем команды могут быть помещены в буфер, что устраняет необходимость повторной выборки (“re-fetch”) тех же самых команд из памяти на последующих итерациях. Это может привести к значительному увеличению скорости исполнения цикла, так как обращение к буферу осуществляется за один такт процессора. Никаких дополнительных программных настроек для работы буфера цикла не требуется — важно лишь знать его объем, поскольку максимальная эффективность будет достигнута, когда число команд в цикле меньше этого значения.

Кэшируемая память

В типичных ЦСП объем быстрой внутренней памяти обычно мал. В свою очередь, микроконтроллеры обычно имеют доступ к большим областям внешней памяти. Иерархиче-

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

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

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

Кэш команд обычно работает по принципу LRU (Least Recently Used), когда наиболее часто исполняемые команды замещаются реже других. В ряде случаев оптимальной производительности системы можно добиться, настроив, как показано на рис. 3, часть внутренней памяти данных как кэш, а часть — как SRAM. В такой конфигурации контроллеры DMA пересылают данные в ядро напрямую, а табличные данные подгружаются в кэш данных по мере необходимости.

Многофункциональные команды

Производительность процессоров обычно оценивается в миллионах команд, которые они могут выполнить в секунду (MIPS, millions instructions per second). Однако из-за разночтений в определении того, что представляет собой команда, для современных процессоров такой способ оценки не очень подходит. Например, многофункциональные команды, которые раньше можно было встретить только в дорогих параллельных процессорах, теперь доступны и в недорогих процессорах с фиксированной точкой. С помощью таких команд процессоры могут в одном процессорном цикле выполнять несколько операций АЛУ/умножителя-накопителя (MAC), а также операции загрузки данных из памяти и сохранения данных в память. Память в процессоре обычно разбита на «суббанки»,

R1.H=(A1+=R0.H*R2.H), R1.L=(A0+=R0.L*R2.L) || R2 = [I0-] || [I1++]=R1;

R1.H=(A1+=R0.H*R2.H),R1.L=(A0+=R0.L*R2.L)

- умножение R0.H*R2.H

- накопление результата в A1

- сохранение результата в R1.ni - умножение R0.L*R2.L

- накопление результата в АО

- сохранение результата в R1.L

[I1++] = R1

- сохранение двух регистров, R1.ni и R1.L,

в память для использования в следующей команде

- инкремент регистра указателя 11 на 4 Память

Р2 = [10—]

- загрузка двух 16-разрядных регистров, К2.Н и Р2.Ц из памяти для использования в следующей команде

- декремент регистра указателя Ю на 4

Рис. 4. Выполнение нескольких операций за один процессорный цикл при помощи многофункциональных команд процессора Blackfin

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

Пример многофункциональной команды приведен на рис. 4. Как показано на нем, в одном процессорном цикле помимо двух независимых операций умножения-накопления также может выполняться выборка данных из памяти и сохранение данных в память.

Конвейер с внутренней блокировкой

По мере увеличения скорости процессора неизбежно возрастает глубина (количество уровней) конвейера. Это очень важно понимать, поскольку эффекты конвейера могут сильно усложнить написание программ на языке ассемблера. Некоторые процессоры имеют конвейер с внутренней блокировкой (“interlocked” pipeline). В этом случае при написании программы на языке ассемблера программисту не потребуется заниматься планированием или отслеживанием продвижения данных и команд по конвейеру — процессор автоматически отрабатывает остановы конвейера и другие ситуации, при которых последовательный процесс исполнения программы нарушается.

Гибкий регистровый файл данных

И, наконец, еще одной особенностью современных ЦСП является наличие универсальных регистров данных. В традиционных ЦСП с фиксированной точкой размер слов обычно фиксирован. В то же время, возможность работы с регистрами данных, в зависимости от задачи, как с одним 32-разрядным словом (например, R0), либо как с двумя 16разрядными словами (R0.L и R0.H — младшая и старшая половины регистра соответственно) дает свои преимущества. В системах с двумя MAC это свойство позволяет в одном процессорном цикле выполнять операции над четырьмя 16-разрядными операндами.

Анализ и сравнение кода

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

тех правил, которыми руководствуется программист, пишущий на языке ассемблера. В приводимых примерах для иллюстрации использовался язык ассемблера процессоров Blackfin.

Скалярное произведение

Скалярное произведение — это операция, позволяющая измерить степень ортогональности двух векторов. Большинству С-про-граммистов знакома следующая реализация скалярного произведения:

short dot(short a[], short b[], int size)

{

int i; int output = 0; for(i=0; i<size; i++)

{ output += (a[i] * b[i]); } return output;

}

Основная часть программы на языке ассемблера выглядит следующим образом:

//P0 = счетчик циклов, I0 иР1 — регистры адреса А1 = А0 = 0; //A0 иА1 — аккумуляторы

LSETUP(loop1,loop1) LC0 = Р0; //Настройка аппаратного цикла, //начинающегося с метки loopl loopl: A1 += R1.H * R0.H, А0 += R1.L * R0.L II R1 = [P1 ++] II R0 = [I0 ++] ;

Отметим архитектурные особенности ЦСП, за счет которых достигается такой малый объем кода.

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

чала следующей итерации цикла в конце каждой предыдущей итерации необходима команда перехода. В приведенной программе на языке ассемблера для организации цикла требуется всего одна команда — ЬЗЕТИР.

Многофункциональные команды позволяют в одном процессорном цикле выполнять две арифметические команды и два обращения к данным. На каждой итерации цикла необходимо извлечь из памяти значения а[1] и Ь[1], перемножить их и, наконец, прибавить результат к текущему значению суммы. На многих микроконтроллерных платформах для выполнения этих операций потребовалось бы четыре команды. Последняя строка программы на языке ассемблера показывает, что в ЦСП все эти операции могут быть выполнены за один процессорный цикл.

Параллельные операции АЛУ позволяют одновременно выполнять две 16-разрядные команды. В приведенной программе на языке ассемблера на каждой итерации цикла используются два аккумулятора (А0 и А1). За счет этого число необходимых итераций уменьшается на 50%, и, следовательно, время, затрачиваемое на выполнение цикла, уменьшается вдвое.

Фильтр с конечной импульсной характеристикой

Фильтр с конечной импульсной характеристикой (КИХ-фильтр) — это очень распространенный тип фильтра, выходной сигнал которого представляет собой свертку отсчетов входного сигнала с коэффициентами фильтра. Прямая реализация фильтра на языке С очень похожа на программу для определения скалярного произведения:

//отсчет сигнала помещается в циклический буфер x[cur] = sampling_function();

cur = (cur+1)%TAPS; // циклическое смещение указателя cur //выполнение умножения-накопления

у = 0;

for(k=0;k<TAPS;k++)

{

у += h[k] * x[(cur+k)%TAPS];

}

Основная часть программы КИХ-фильтра на языке ассемблера похожа на программу, реализующую скалярное произведение. Действительно, в ней для достижения максимальной скорости выполнения применяются те же архитектурные особенности ЦСП. В данном конкретном примере регистр R0 используется для промежуточного хранения отсчетов сигнала, а регистр R1 — коэффициентов фильтра:

//Р0 содержит количество звеньев фильтра R0=[I0++] II R1 = [I1++]; //Инициализация R0 иR1

A1=A0=0; //Обнуление аккумуляторов

LSETUP (loopl, loopl) LC0 = P0; //Настройка внутреннего цикла loopl: A1+=R0. L*R1.L, A0+=R0.H*R1.H II R0 = [I0++] II R1 = [I1++]; //Вычисление

Помимо архитектурных особенностей процессора, отмеченных для программы скалярного произведения, в программе КИХ-фильтра также используется циклическая буферизация.

Поддержка циклических буферов устраняет необходимость явного применения арифметики по модулю. В языке С циклическая буферизация реализуется при помощи оператора % (оператор деления по модулю). Из приведенной программы на языке ассемблера следует, что этот оператор не транслируется в дополнительную команду в теле цикла. Вместо этого регистры 10 и II генератора адреса данных конфигурируются вне цикла таким образом, что при достижении границы буфера коэффициентов происходит автоматический возврат указателей к его началу.

Быстрое преобразование Фурье

Быстрое преобразование Фурье (БПФ) — это неотъемлемая часть многих алгоритмов обработки сигналов. Одна из его любопытных особенностей заключается в том, что при последовательном во времени порядке отсчетов входного массива выходной массив будет иметь бит-реверсный порядок. Для большинства традиционных процессоров общего назначения от программиста потребуется реализовать отдельную процедуру для пере-упорядочивания выходного массива. На платформе ЦСП поддержка бит-реверсии адреса встроена в генератор адресов.

Бит-реверсная адресация устраняет необходимость в отдельной процедуре бит-ре-

версии при реализации БПФ. Возможность автоматической аппаратной бит-реверсной адресации элементов выходного массива БПФ избавляет программиста от написания дополнительных подпрограмм, и, следовательно, приводит к росту производительности.

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

Очевидно, что при выборе стратегии программирования для приложения на базе ЦСП необходимо учесть много факторов. В большинстве случаев использование языка С/С++ с эффективным компилятором/оптимизатором дает неплохие результаты, но максимальной производительности от процессора можно добиться, как правило, лишь при написании программы на языке ассемблера вручную. Однако делать это стоит только при условии хорошего понимания принципов работы архитектурных компонентов, обеспечивающих эффективное кодирование. ■

i Надоели баннеры? Вы всегда можете отключить рекламу.