![источник: https://innovas-services.fr/solving-business-problems/ источник: https://innovas-services.fr/solving-business-problems/](https://habrastorage.org/getpro/habr/upload_files/8a2/d49/809/8a2d4980984de84f14da11495dc823f8.png)
DSP-процессоры: назначение и особенности
Большинство из нас в повседневной жизни постоянно сталкивается с различными компьютерными системами: процессорами общего назначения (general-purpose, в основном x86) в ноутбуках и рабочих станциях, их мощными многоядерными версиями в датацентрах, мобильными процессорами в телефонах, многочисленными контроллерами в бытовой технике и на транспорте. Но помимо всех упомянутых вариантов есть ещё одно важное, хотя и редко упоминаемое семейство: цифровые сигнальные процессоры, чаще именуемые Digital Signal Processors или просто DSP.
Именно DSP решают задачи обработки больших объёмов информации в реальном времени, возникающие при передаче данных (звонков и мобильного Интернета) в мобильных сетях, обработке фотографий и восстановлению звука. Даже в топовых телефонах вся эта работа выполняется не на мощных ARM-ядрах, а на специализированных DSP.
В этой статье будет кратко изложена история DSP, их отличие от процессоров общего назначения, особенности их архитектуры, а также будет подробно рассказано о способах оптимизации кода.
История
Первые DSP появились в 1970-х годах. Эти процессоры стали логичным развитием специализированных аналогово-цифровых устройств, предназначенных для обработки речи, прежде всего её кодирования и фильтрации (прорыв в соответствующих научно-технических отраслях стал возможен благодаря спросу на эти технологии в годы Второй Мировой войны). Трудоемкость и сложность разработки устройств под каждую возникающую задачу, а также успехи в развитии электронной базы (широкое распространение технологии MOSFET) и математических алгоритмов (БПФ, цифровая фильтрация) привели к возможности создания универсальных, т.е. программируемых, цифровых процессоров, которые могли быть с помощью программ адаптированы для широкого класса задач. Адаптируемость на практике означала снижение стоимости разработок, сокращение времени выхода на рынок (time-to-market), возможность послепродажного обновления алгоритма для устранения ошибок, возможность поддержки новых требований пользователей. Во многих случаях эти возможности с лихвой компенсировали ухудшение производительности по сравнению со специальными ускорителями.
![Рис. 1 Первый крупный успех DSP: планшет Speak&Spell (Texas Instruments, 1978) Рис. 1 Первый крупный успех DSP: планшет Speak&Spell (Texas Instruments, 1978)](https://habrastorage.org/getpro/habr/upload_files/352/104/e28/352104e28c4fd5f95aa9e727a2d29e9f.png)
![Рис. 2 С момента появления стандарта GSM DSP являются обязательным компонентом мобильных сетей Рис. 2 С момента появления стандарта GSM DSP являются обязательным компонентом мобильных сетей](https://habrastorage.org/getpro/habr/upload_files/7de/ff8/f12/7deff8f121a10dc27b2cc6c284776376.png)
![Рис. 3 Обработка изображений в камерах (дебайеризация, удаление шумов, фильтрация) также выполняются на DSP (источник: https://snapshot.canon-asia.com/india/article/en/5-things-made-possible-with-digic-image-processor) Рис. 3 Обработка изображений в камерах (дебайеризация, удаление шумов, фильтрация) также выполняются на DSP (источник: https://snapshot.canon-asia.com/india/article/en/5-things-made-possible-with-digic-image-processor)](https://habrastorage.org/getpro/habr/upload_files/250/f63/8f6/250f638f63e4da8d455fc9d1664c9e0c.png)
Из-за необходимости обработки в реальном времени и экономии электроэнергии DSP сильно отличались от процессоров общего назначения. В каком-то смысле они были первым примером программируемых вычислительных ускорителей, т.е. процессоров, максимально эффективно решающих определённый класс задач.
Преимущества DSP
Чем же именно отличаются DSP от обычных мощных процессоров общего назначения, особенно таких мощных как Intel Xeon или Cortex-A, и почему процессоры общего назначения не используют для обработки сигналов? Чтобы ответить на этот вопрос посмотрим на топологию современного процессора от Intel.
![Рис. 4 Intel Skylake (источник: https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client) ) Рис. 4 Intel Skylake (источник: https://en.wikichip.org/wiki/intel/microarchitectures/skylake_(client) )](https://habrastorage.org/getpro/habr/upload_files/667/498/43c/66749843c7a2df425523994a870860d1.png)
Из рисунка мы видим, что значительная часть площади кристалла отводится не под вычислительные ресурсы, а под сложную логику определения зависимостей, спекулятивного исполнения (out-of-order speculative execution) и составления расписания (scheduling). В сумме накладные расходы приводят к тому, что “КПД” процессора, т.е. энергия, затрачиваемая на выполнение реальных вычислений, составляет менее 1%:
While a simple arithmetic operation requires around 0.5–20 pJ, modern cores spend about 2000 pJ to schedule it.
Conventional multicore processors consume 157–707 times more energy than customized hardware designs.
(из статьи “Rise and Fall of Dark Silicon”, приведённой в списке литературы).
Чтобы сделать сравнение более конкретным, возьмём мощный процессор общего назначения от Intel и мощный DSP фирмы Texas Instruments (например Skylake Xeon Platinum 8180M и TMS320C6713BZDP300):
| CPU (Intel) | DSP (TI) |
Частота | 2.5 ГГц | 500 МГц |
Число ядер | 28 | 1 |
Пиковая производительность | 560 GIPS | 1.8 GIPS |
Энергопотребление | 205 Вт | 1 Вт |
Out-of-order | Да | Нет |
Цена | $13K (+ система охлаждения) | $35 (оптовая цена) |
Target applications | Любые | - Большое число циклов - Высокий параллелизм по данным - Регулярный паттерн доступа в память |
Производительность / Вт / ядро | 0.097 GIPS/Вт/ядро | 1.7 GIPS/Вт/ядро (в 17 раз лучше Intel) |
Производительность / Вт / ядро / $ | 0.0075 MIPS/Вт/ядро/$ | 0.051 GIPS/Вт/ядро/$ (в 7000 раз лучше Intel) |
Легко видеть, что хотя абсолютные значения производительности у DSP существенно ниже, удельная производительность на 1 ватт потребляемой мощности выше на 4 (!) порядка. Конечно такое сравнение не совсем корректно: DSP могут достигать пиковой производительности только на весьма специфических задачах обработки сигналов: свёртках, БПФ, фильтрации и т.д. Такие задачи характеризуются высокой регулярностью обрабатываемых данных (плоские массивы в памяти, обрабатываемые в циклах) и большим параллелизмом по данным.
Архитектура DSP
Специфика решаемых задач оказывает существенное влияние на архитектуру и системное ПО для DSP. По словам Jennifer Eyre, аналитика исследовательского центра BDTI, “архитектура DSP формируется теми задачами, которые на них считаются” (“Architecture of DSP is molded by algorithms”, из “Evolution of DSP Processors”). Перечислим особенности таких задач:
Практически бесконечный параллелизм уровня команд (ILP, Instruction Level Parallelism)
Большинство алгоритмов (свертка, быстрое преобразование Фурье, вычисления с комплексными числами) сводятся к выполнению операций сложения и умножения над плотными массивами данных
Вычисление производятся на встроенных системах, с жёсткими требованиями по энергопотреблению
Таким образом, основными целями являются максимальное использование присущего задачам параллелизма и снижение энергопотребления при выполнении циклов.
Для использования ILP используются различные техники:
Векторные инструкции (SIMD, Single Instruction Multiple Data)
Сложные инструкции (CISC, Complex Instruction Set Computer):
Составные математические операции (умножение и вычитание с накоплением, гистограммы, спецфункции, комплексные вычисления)
Операции умножения со сдвигом (для арифметики с фиксированной точкой)
Широкий набор режимов адресации (с шагом, с пре- и пост-инкрементом, циклическим обходом и пр.)
Алгоритмо-специфические операции (например подсчёт контрольных сумм сетевых пакетов, криптография, аудио-видео декодеры)
Расширяемые системы команд (в IP-продуктах Ceva и Tensillica)
Сильно расслоенная память (для выдачи нескольких параллельных загрузок за такт или индексных обращений в память типа scatter/gather)
Избавление от задержек, вызываемых ветвлениями:
Поддержка предикатного выполнения для всех команд процессора (или подавляющего большинства)
Процессорные хинты (специальные инструкции для предзагрузки данных)
Слоты задержки
Быстрые циклы (zero-overhead loops)
Вынесение наиболее вычислительно-ёмких алгоритмов (алгоритм Витерби, БПФ, QR-разложение, нейросетевые вычисления) на встроенные ускорители (т.н. fixed function units)
Ускорение продолжительных операций с памятью с помощью специальных блоков прямого доступа в память (DMA), с поддержкой произвольных 2D/3D-шагов
Для снижения же энергопотребления используются
Переход от действительных чисел на арифметику с фиксированной точкой
Нестандартные типы данных (например 20- и 40-битные целые)
Упорядоченные (in-order) вычисления, отсутствие спекулятивности (speculation) и внеочередного выполнения (out-of-order)
Явное формирование параллельно исполняемых пакетов инструкций компилятором (VLIW)
Отсутствие отслеживания процессором зависимостей между инструкциями и перенос ответственности за точное планирование инструкций на компилятор (exposed pipeline)
Отсутствие кэшей
Прямое обращение в глобальную DRAM-память приведёт к остановке процессора для выполнения транзакции
Вместо instruction- и data-кэшей используется небольшая (до 1 Мбайта) быстрая SRAM-память, т.н. scratchpad или Tightly Coupled Memory, загрузками в которую явно управляет программист
Для упрощения таких загрузок часто используются оверлеи – специальный механизм разбиения программы на независимые участки, которые могут по требованию динамически подгружаться в TCM, вытесняя друг друга
Вместо таблиц предсказания ветвлений (branch target buffer, BTB) в DSP используются различные техники для амортизации ветвлений (слоты задержки, хинты, быстрые циклы)
Кластерные регистровые файлы (т.е. разбиение регистрового файла на блоки, регистры которых не могут использоваться вместе в одной инструкции)
В процессорах общего назначения каждое из указанных решений привело бы к фатальному ухудшению производительности и удобства работы, но в случае DSP особенности применения снимают эту проблему.
Некоторые из указанных подходов можно видеть ниже на примере кода для процессора Texas Instruments:
; Сложение векторов
L2:
LDW .D1T1 *A4++, A3
|| LDW .D2T2 *B4++, B5 ; Две параллельные загрузки с постинкрементом
NOP 3 ; Явная задержка для ожидания загрузок
BDEC .S2 L2, B0 ; Инструкция быстрого цикла (декремент + сравнение + goto)
ADD .L1X B5, A3, A3 ; Слот задержки 1
STW .D1T1 A3, *A5++ ; Слот задержки 2
В частности можно видеть
инструкцию быстрого цикла
BDEC
, сочетающую в себе декремент индекса цикла, сравнение с 0 и условный переходявное указание пареллелизма с помощью лексемы
||
явные задержки инструкций с помощью
NOP
использование слотов задержки
Разработка под DSP
Разработка программ для DSP также имеет свою специфику. Их размеры как правило небольшие, а сложность невелика (используются простые структуры данных, отсутствуют рекурсии и динамические аллокации), поэтому их оптимизации уделяется существенно больше времени, чем в случае программ для процессоров общего назначения. Собственно говоря, оптимизация под конкретный целевой процессор, чаще всего, и является доминирующей фазой в разработке ПО под DSP.
В частности, при оптимизации под DSP намного более распространены агрессивные режимы компиляции: LTO (link-time optimization) и PGO/FDO (profile-guided/feedback-driven optimization). Важные циклы аннотируются прагмами для явного контроля векторизации и развертки, а указатели помечаются атрибутами restrict/noalias, чтобы позволить компилятору удалять максимально возможное число зависимостей.
Также код часто векторизуют вручную и переписывают с использованием функций-интринсиков (intrinsic), т.е. мнемонических аналогов ассемблерных команд, которые позволяют явно контролировать выбор инструкции в порождаемом ассемблере, не полагаясь на способности конкретной версии компилятора. Такой подход может показаться чрезмерным усложнением, но даже он представляет собой существенный прогресс по сравнению с повсеместно использовавшимся ручным кодированием на ассемблере (которое также до сих пор встречается).
Сложные технологии оптимизации, такие как автоматические векторизация и распараллеливание, напротив, менее популярны из-за их непредсказуемости и нестабильности: из-за хрупкости соответствующих алгоритмов каждая новая версия компилятора может перестать оптимизировать какой-нибудь важный цикл.
Компилятор и профилировщик, как правило, оснащены механизмами для выдачи отчётов о проведённых оптимизациях и рекомендаций по улучшению кода: добавлению прагм, использованию оптимальных типов данных и пр. (для реализации подобного функционала в Nvidia Nsight даже используется полноценная экспертная система). Анализ и отработка таких рекомендаций является важной частью работ по оптимизации кода.
Стоит отметить, что оптимизация как правило идёт в тесном контакте и поддержке со стороны поставщика DSP, а иногда и напрямую выполняется его сотрудниками.
В пакет разработки DSP наряду со стандартными компонентами (компилятором, ассемблером, оптимизирующим редактором связей, профилировщиком, отладчиком и IDE) часто также входят специфические программы:
симуляторы (потактовые и функциональные)
host-библиотека с моделями intrinsic-функций, которые позволяют начать разработку и функциональное тестирование кода ещё до готовности симуляторов (а для сложных и долгих сценариев, которые нельзя моделировать на симуляторе из-за длительного времени ожидания – до готовности “железа”)
оптимизирующий ассемблер (“linear assembler”), позволяющий автоматизировать часть работ по написанию ассемблерного кода (планирование инструкций, выделение регистров)
автотюнер, т.е. более или менее интеллектуальный переборщик флагов компиляции и source-level директив для поиска оптимальной комбинации (иногда автотюнер прямо интегрируется в компилятор)
генераторы boilerplate-кода (например RPC-кода для вызова функций DSP с управляющего микроконтроллера)
Компиляторы DSP
Компилятор является неотъемлемым и важнейшим компонентом DSP как продукта. В случае процессоров общего назначения разница между плохим и хорошим компилятором обычно невелика – производительность режима –O2 редко превосходит –O0 более чем в два-три раза (т.к. процессор компенсирует неэффективность с помощью out-of-order выполнения). В то же время для DSP этот разброс легко достигает десятка. Кроме того, сложность программирования на ассемблере гораздо выше, поэтому переписывание на нём всего performance-critical кода обычно невозможно.
Компиляторы DSP как правило основаны на open-source решениях:
Open64 (ранние продукты Ceva и Cadence/Tensilica)
GCC (Texas Instruments и ранние продукты Qualcomm)
LLVM (новые продукты Ceva, TI и Qualcomm, возможно Cadence/Tensilica)
В основном это связано со сложностью самостоятельной поддержки новых версий C++, подходящей для коммерческого использования лицензией и поддержкой новых языков, таких как OpenCL, OpenMP/OpenACC и Halide.
По сравнению с обычными процессорами в случае DSP приходится значительно больше времени уделять разработке бэкэнда, т.е. низкоуровневой части компилятора (например, бэкэнды DSP Hexagon и AMDGPU занимают столько же места, сколько значительно более активно разрабатываемый бэкэнд для AArch64). Это связано с тем, что для архитектур VLIW требуется гораздо более точные модели конвейера и размеров инструкций: в отличие от процессоров общего назначения здесь любая неточность приведёт либо к генерации некорректного кода, либо ко вставке избыточных NOP’ов и неэффективной работе. В период разработки процессоров Intel Itanium, на которых была впервые опробована используемая в DSP архитектура VLIW, даже возник мем о том, что для компиляции под такие архитектуры требуются “супер-компиляторы” (“heroic compiler”, подразумевалось, что такие компиляторы разработать невозможно). В действительности технологии компиляции с тех пор значительно улучшились, в том числе специально для DSP были разработаны новые алгоритмы (например метод аллокации регистров с помощью Partitioned Boolean Quadratic Programming).
Расширения языка
В контексте оптимизаций, работа компилятора сводится к поиску максимального количества инструкций, которые можно было бы выполнить параллельно. Как уже упоминалось выше, пользователь может помочь компилятору с помощью различных директив:
// Указание диапазона для числа итераций цикла
// (позволяет избежать генерации пролога при развертке цикла)
#pragma MUST_ITERATE(min, max, multiple)
for (i = x; i < y; ++i)
...
// Функция не меняет глобальные переменные
#pragma FUNC_NO_GLOBAL_ASG(func)
extern void foo();
// Функция не меняет переменные через сохраненные ранее указатели
#pragma FUNC_NO_IND_ASG(func)
extern void bar();
// i гарантированно выровнена на 8
_nassert(i & 3 == 0);
Также активно применяются специальные инструкции, например, для предзагрузки данных (типа __builtin_prefetch
в GCC) или для указания банков памяти, адресуемых указателями (чтобы компилятор мог запланировать их параллельное выполнение, не рискуя вызвать банк-конфликт).
Кроме того, в некоторых компиляторах можно использовать более агрессивные (по сравнению со стандартом языка Си) правила алиасинга указателей, что избавляет пользователя от явной расстановки атрибутов restrict в коде. Как правило можно выбирать такие расширения:
Различные адресные параметры функции всегда указывают на непересекающиеся участки памяти
Адресные переменные никогда не указывают на статические переменные
Функции не кэшируют переданные адреса
В некоторых компиляторах есть и более агрессивные опции вплоть до полного отключения алиасинга (в стиле флага -Msafeptr=all компиляторов PGI).
Как уже упоминалось, компилятор поддерживает программирование векторных вычислений с помощью интринсиков. Пример такого кода:
int32x4_t acc = 0;
int *p = ..., coeff = ...;
for (i = 0; i < N; i += 4) {
int32x4_t x = vload(&p[i]);
acc = vmac(acc, coeff, x);
// acc += coeff * x; Менее надёжный вариант, т.к. полагается на оптимальную работу компилятора
}
С помощью intrinsic-функций обычно предоставляется доступ ко всем векторным инструкциям. К недостаткам такого решения относится привязка к определённому вендору и модели процессора (последнее скорее некритично, т.к. поставщики стараются предоставлять конвертеры унаследованного векторного кода для новых моделей процессора).
Оптимизации
При компиляции для DSP помимо традиционных (встраивание функций, инварианты циклов, эффективное планирование, скалярные оптимизации и пр.) особую важность играют несколько ключевых оптимизаций:
Развертка (unrolling) и конвейеризация циклов (software pipelining)
Предикативное выполнение (if-conversion)
Переименование индуктивных переменных (induction variable renaming)
Остановимся на них немного подробнее.
При конвейеризации происходит совмещение нескольких итераций исходного цикла в одной итерации преобразованного, чтобы с помощью наложения инструкций можно было скрыть их задержки. Так цикл вида
// Исходный код
for (i = 0; i < N; ++i)
a[i] = b[i] * 3;
// Ассемблер
ld (a0)+, a2
nop 3
mul a2, a3, a4
st a4, (a1)+
можно было бы конвейеризовать как
// Исходный код
tmp2 = b[0] * 3; tmp1 = b[1];
for (i = 0; i < N - 2; ++i) {
a[i] = tmp2
tmp2 = tmp1 * 3;
tmp1 = b[i + 2]
}
a[N - 2] = tmp2; a[N - 1] = tmp1 * 3;
// Ассемблер
st a4, (a1)+ || ld (a0)+, a2 || mul a2, a3, a4
сократив время итерации с четырех тактов до одного.
При предикативном выполнении мы заменяем блоки кода с ветвлениями на один блок, вычисления в котором выполняются по условию. Схематично такое преобразование заменит код
for (i = ...) {
if (p[i])
x[i] = a * y;
else
x[i] = b * z;
}
на
for (i = ...) {
bool predicate = p[i];
tmp1 = predicate ? a * y : 0;
tmp2 = predicate ? b * z : 0;
x[i] = predicate ? tmp1 : tmp2;
}
или даже, если не стоит цель оптимизации энергопотребления, на
for (i = ...) {
bool predicate = p[i];
tmp1 = a * y;
tmp2 = b * z;
x[i] = predicate ? tmp1 : tmp2;
}
В результате этой замены компилятор сможет для условной инструкции породить код
cmp a7, 0, p1
mul a0, a1, a2, p1
mul a3, a4, a2, !p1
Здесь в процессор поступят обе инструкции умножения, но реально выполнена будет только одна, в зависимости от значения предикатного регистра p1
. Такой “линеаризованный” код не только работает быстрее (из-за отсутствия ветвлений), но и позволяет осуществлять другие оптимизации (ту же конвейеризацию циклов).
Наконец, переименование индуктивных переменных позволяет избавиться от лишних зависимостей, которые часто возникают при развертке циклов. Так развернутый вдвое цикл
// Unroll 2
for (i = ...) {
z[i] = a * x[i]
++i;
z[i] = a * x[i];
++i;
}
преобразуется этой оптимизацией к
// Dependencies removed
for (i1, i2 = ...) {
z[i1] = a * x[i1]
i1 += 2;
z[i2] = a * x[i2];
i2 += 2;
}
В данном случае переименование индексной переменной позволяет нам выполнять инструкции, относящиеся к i1
и i2
, параллельно.
Рекомендуемая литература
История:
Недостатки процессоров общего назначения:
Архитектура VLIW
J.A. Fisher et al. “Embedded Computing: A VLIW Approach to Architecture, Compilers and Tools” (Джозеф Фишер - создатель концепции VLIW и первого в мире VLIW-процессора Multiflow)
Лекции по “Mill computing” на YouTube (и сайт проекта)
Компиляторы VLIW
Компиляторные руководства Texas Instrument (User и особенно Programmer guides, а также App reports)
Архитектура DSP
Компиляторы DSP (статьи E. Belaish, CEVA)
Об авторе
![](https://habrastorage.org/getpro/habr/upload_files/bcd/8f5/be6/bcd8f5be6bc4abc0218be9ff9af191f0.png)
Юрий Грибов, Senior Engineer, System-on-Chip SW Team, Исследовательский центр Samsung
Автор выражает большую признательность рецензентам из Исследовательского центра Samsung: Алексею Пущину, Михаилу Черкашину и Павлу Копылу.
Компания Samsung выпускает мобильные системы на кристалле Exynos со встроенными DSP и NPU. Они используются, например, для обработки изображений с камеры смартфона. В московском Исследовательском центре Samsung разрабатывают state-of-the-art компиляторы для таких устройств, и мы активно ищем людей в нашу команду. Прямо сейчас в нашем центре открыто несколько вакансий для соискателей разного уровня подготовки: от стажеров до разработчиков сениор-уровня. Узнать подробнее о работе и вакансиях нашего отдела можно в интервью моего руководителя на Хабре.