Технология MIPS SIMD и процессор Байкал-Т1
Коллеги из Байкал Электроникс предложили поработать с процессором Байкал-Т1 [L1] и написать о своих впечатлениях. Для них это способ рассказать разработчикам о возможностях и особенностях своего процессора. Для меня — шанс поближе познакомиться с системой на современном процессорном ядре и в будущем изобретать поменьше «велосипедов», добавляя, к примеру, новую функциональность в проект MIPSfpga-plus [L2]. Ну и обычное инженерное любопытство, опять же…
Сегодня речь пойдет о векторном расширении архитектуры MIPS SIMD, которое доступно в ядрах
MIPS Warrior P-class P5600 [L3], а значит присутствует и в процессоре Байкал-Т1. Статья ориентирована на начинающих разработчиков.
Введение
Практически всегда с разработкой того или иного устройства (прибора/аппаратуры/программно-аппаратного комплекса и т.д.) связано решение задачи по обработке цифровых и аналоговых сигналов. На входе могут быть показания датчиков, сигналы с устройств ввода-вывода, информация из файла на диске и т.д. На выходе: изображение на мониторе, звук из колонок, сигналы управления приводами, показания индикаторов на приборной панели и пр. А «между» вводом и выводом — набор тех или иных математических операций.
Если кратко перечислить способы реализовать эту «математику в железе», то получим следующий список доступных разработчику инструментов, которые могут быть применены как вместе, так и по отдельности:
- Реализация в виде аналоговой схемы. Несмотря на засилье цифровых устройств, мир и органы чувств человека все равно остаются аналоговым. Хотим мы этого или нет, но даже обрабатывая информацию «исключительно» в цифре, перед входом АЦП нам все равно приходится ставить фильтр. Точно также на аналоговых компонентах могут быть реализованы интеграторы, дифференциаторы, сумматоры и т.д. За десятилетия господства аналоговой электроники инженеры накопили огромный опыт решения тех или иных задач и хороший разработчик (пусть даже цифровой электроники) учитывает это наследие [D1];
- Программная реализация на микроконтроллере. Обрабатываемых сигналов немного, а математика — не сложная или не требовательная к ресурсам? В этом случае относительно недорогой микроконтроллер с его АЦП, невысокой частотой работы, возможностями энергосбережения — вполне себе вариант. При необходимости «узкие» места можно написать на ассемблере;
- Реализация на ПЛИС. Если у нас высокие требования к скорости, параллелизму, масштабированию решения, то описываем математику в виде модуля на Verilog или VHDL, выбираем ПЛИС, которая может работать на необходимой для обработки частоте. Если решение окажется очень удачным и появится смысл в его широком тиражировании — добро пожаловать в мир ASIC [L24];
- Программно-аппаратная реализация в виде системы на кристалле. Система слишком сложная, чтобы описывать ее целиком на Verilog, отдельную логику хочется запрограммировать на высокоуровневом языке, да и вообще — управлять всем из Linux? В этом случае вариантом решения для нас является СнК (System-on-a-Chip, SoC): берем готовое процессорное ядро (Nios II, MIPSfpga и т.д.) и обвешиваем его необходимыми нам периферийными модулями, среди которых будет наш особенный, выполняющий хитрую математику. Некоторые операции можно сделать доступными в виде процессорных команд [L4]. И да, в перспективе это тоже можно реализовать в ASIC;
- Использование цифрового сигнального процессора (ЦСП). Здесь мы, фактически, покупаем готовую микросхему с процессорным ядром, своим набором периферии и набором команд, специально ориентированным на высокоскоростную цифровую обработку сигналов. Вокруг нее мы и выстраиваем свое решение [L10, L5];
- Программная реализация на процессоре общего назначения. Каждый из производителей процессоров предлагает свои архитектурные решения, позволяющие оптимизировать выполнение тех или иных математических операций. И задача разработчика ПО при необходимости использовать предлагаемые производителем возможности для ускорения вычислений. Как раз об этом применительно к процессорам MIPS и пойдет речь ниже;
- Использование графического контроллера для вычислений. Для того, чтобы настоящий список был более полным, нельзя не упомянуть о возможности вынести наиболее сложные и ресурсоемкие вычисления на видеокарту [L6, L7].
Идеальных инструментов не бывает. Самый лучший инструмент — это тот, который гарантирует решение задачи в приемлемые сроки, для которого в проектной команде есть необходимые компетенции, и который либо есть в наличии, либо может быть приобретен с минимальными затратами. На принятие подобных решений накладываются ограничения бюджета, требования заказчика, а иногда и политические причины.
Надеюсь, что данная статья будет полезна в качестве вводной для тех читателей, кто столкнется с необходимостью оптимизировать выполнение тех или иных вычислений на процессоре Байкал-Т1 или ином другом, построенном на базе ядра (ядер) MIPS, где доступна технология MIPS SIMD.
Ресурсоемкость вычислений
Перед тем, как двигаться дальше, рассмотрим одну из часто встречающихся задач цифровой обработки сигналов (ЦОС) — фильтрацию. В качестве примера возьмем фильтр с конечной импульсной характеристикой (КИХ, FIR, finite impulse response) [L8]. Не углубляясь в теорию ЦОС и математические выкладки отметим основное — уравнение, которое описывает данный вид цифровых фильтров:
где x(n) — входной сигнал, y(n) — выходной сигнал, P — порядок фильтра, bi — коэффициенты фильтра.
Это же уравнение можно записать следующим образом:
Природу входного сигнала x(n) в данном случае — проигнорируем. Пусть это будут данные, полученные с АЦП, но с тем же успехом они могут быть считаны из файла. Для нас в рассматриваемом случае это не имеет никакого значения. Так как наша текущая статья «про вычисления», а не «про ЦОС», то, соответственно, не будем погружаться и в Магию фильтров, а просто воспользуемся одним из онлайн сервисов для расчета коэффициентов [L9]:
Устанавливаем желаемые параметры фильтрации (например, один из predefined вариантов: band stop — режекторный фильтр [L11]) и нажимаем кнопку Design Filter:
Результатом расчета является АЧХ фильтра [L12]:
Набор коэффициентов и исходный код:
SampleFilter.h
#ifndef SAMPLEFILTER_H_
#define SAMPLEFILTER_H_
/*
FIR filter designed with
http://t-filter.appspot.com
sampling frequency: 2000 Hz
* 0 Hz - 200 Hz
gain = 1
desired ripple = 5 dB
actual ripple = 3.1077303934211127 dB
* 300 Hz - 500 Hz
gain = 0
desired attenuation = -40 dB
actual attenuation = -42.49314043914754 dB
* 600 Hz - 1000 Hz
gain = 1
desired ripple = 5 dB
actual ripple = 3.1077303934211127 dB
*/
#define SAMPLEFILTER_TAP_NUM 25
typedef struct {
double history[SAMPLEFILTER_TAP_NUM];
unsigned int last_index;
} SampleFilter;
void SampleFilter_init(SampleFilter* f);
void SampleFilter_put(SampleFilter* f, double input);
double SampleFilter_get(SampleFilter* f);
#endif
SampleFilter.с
#include "SampleFilter.h"
static double filter_taps[SAMPLEFILTER_TAP_NUM] = {
0.037391727827352596, -0.03299884552335979,
0.044230583967321345, 0.0023050970833628304,
-0.06768087195950104, -0.046347105409124706,
-0.011717387509232432, -0.0707342284185183,
-0.049766517282999544, 0.16086413543836361,
0.21561058688743148, -0.10159456907827959,
0.6638637561392535, -0.10159456907827959,
0.21561058688743148, 0.16086413543836361,
-0.049766517282999544, -0.0707342284185183,
-0.011717387509232432, -0.046347105409124706,
-0.06768087195950104, 0.0023050970833628304,
0.044230583967321345, -0.03299884552335979,
0.037391727827352596
};
void SampleFilter_init(SampleFilter* f) {
int i;
for(i = 0; i < SAMPLEFILTER_TAP_NUM; ++i)
f->history[i] = 0;
f->last_index = 0;
}
void SampleFilter_put(SampleFilter* f, double input) {
f->history[f->last_index++] = input;
if(f->last_index == SAMPLEFILTER_TAP_NUM)
f->last_index = 0;
}
double SampleFilter_get(SampleFilter* f) {
double acc = 0;
int index = f->last_index, i;
for(i = 0; i < SAMPLEFILTER_TAP_NUM; ++i) {
index = index != 0 ? index-1 : SAMPLEFILTER_TAP_NUM-1;
acc += f->history[index] * filter_taps[i];
};
return acc;
}
Посмотрим на параметры фильтрации, функцию SampleFilter_get, вспомним приведенное выше уравнение КИХ-фильтра и отметим наиболее важные для нас моменты:
- для того, чтобы получить один отсчет y(n), что в нашем случае равносильно одному вызову функции SampleFilter_get нам необходимо в цикле обработать 25 входных отсчетов x(n) (порядок фильтра, см. макрос SAMPLEFILTER_TAP_NUM и «taps» на скриншоте интерфейса);
- если мы хотим, чтобы эта обработка выполнялась налету (к примеру, по мере поступления x(n) с АЦП), то мы должны успевать выполнять ее с учетом sampling frequency, которая в нашем случае 2000Hz;
- для каждого из этих 25 отсчетов x(n) мы выполняем ряд математических операций: умножение на соответствующий ему коэффициент, сложение в регистр-аккумулятор. Кроме того, не забываем о сопутствующих операциях по чтению/записи из/в память, на которые также требуется время.
Теперь предположим, что условия задачи в силу неких объективных причин были уточнены:
И в результате мы получаем следующую АЧХ:
Что для нас важно:
- в связи с увеличением sampling frequency до 4000Hz в 2 раза сократилось время, которое мы можем потратить на вычисления, если хотим получать результаты налету;
- количество итераций (taps, порядок фильтра) при этом увеличилось почти в 3 раза (с 25 до 67). Если не вносить изменения в параметр неравномерности АЧХ (оставить ripple/att. = 5 dB), то количество итераций все равно увеличится в 2 раза;
- таким образом, незначительное изменение параметров фильтрации привело к увеличению ресурсоемкости вычислений в 6 раз (либо в 4).
Мне бы не хотелось, чтобы читатель обращал внимание на приведенные в примере абсолютные значения параметров фильтра. Основная мысль, которую хотелось бы донести, заключается в том, что в какой-то момент увеличение ресурсоемкости вычислений может выйти за ранее спрогнозированные пределы. И после небольшого изменения алгоритма, его параметров, или входных данных, вдруг, может оказаться что ваше процессорное ядро занимается только тем, что «лопатит» данные, да и то не успевает это делать, не говоря уже про выполнение остальных задач. Либо оно успешно справляется, но создаваемая при этом нагрузка или длительность вычислений уже не соответствуют требованиям к системе. И в этот момент перед вами как никогда встает задача оптимизации.
Скорость вычислений
После того, как мы на простом примере увидели проблему, посмотрим на ее решение. Производители процессоров идут на ряд ухищрений для того, чтобы повысить скорость вычислений: повышают частоту, увеличивают число процессорных ядер, добавляют новые команды, экспериментируют с конфигурацией конвейера, объемом кэша, переходят к использованию все более и более высокоскоростных шин и интерфейсов.
Также никто не забирает у разработчика право реализовать наиболее узкие места на ассемблере, поэкспериментировать с опциями компилятора, сменить алгоритм на менее ресурсоемкий либо ведущий себя более предсказуемо на том же диапазоне параметров.
Cконцентрируемся на двух способах повысить скорость обработки, которые не могут быть использованы без поддержки со стороны архитектуры процессора:
- объединение нескольких арифметических операций в одну команду;
- реализация SIMD-подхода.
Объединение арифметических операций
Вернемся к нашему фильтру и внимательно посмотрим на код функции SampleFilter_get:
double SampleFilter_get(SampleFilter* f) {
double acc = 0;
int index = f->last_index, i;
for(i = 0; i < SAMPLEFILTER_TAP_NUM; ++i) {
index = index != 0 ? index-1 : SAMPLEFILTER_TAP_NUM-1;
acc += f->history[index] * filter_taps[i];
};
return acc;
}
И особенно на строку:
acc += f->history[index] * filter_taps[i];
Здесь мы видим 2 последовательно выполняемые операции: умножение на коэффициент и накопление результатов этой команды в переменной-аккумуляторе. Подобное сочетание умножения и сложения очень часто встречается в алгоритмах ЦОС. Но если эти две операции так часто соседствуют друг с другом, то почему бы их не объединить в одну команду, выполняемую в течении 1 такта? Идея такого объединения пришла в головы инженеров очень давно. Так появилась команда «умножение с накоплением» (совмещённое умножение-сложение, multiply–accumulate operation, MAC), которая ныне присутствует во всех цифровых сигнальных процессорах:
SIMD-подход
Подойдем к решению с другой стороны. А почему бы нам, вместо того, чтобы работать с каждым отсчетом x(n) по отдельности, не объединить несколько отсчетов в один вектор (массив) и применить команду сразу ко всему вектору, а если точнее — то одновременно к каждому элементу вектора? В этом случае за один такт (не считая работу с памятью) можно будет обработать сразу несколько отсчетов:
И чем больше будет максимально допустимый размер вектора, тем выше будет скорость обработки данных. Данный принцип организации вычислений называется SIMD (single instruction, multiple data; одиночный поток команд, множественный поток данных) [L13]. На этом подходе строится работа векторных процессоров [L14] и векторных расширений к скалярным процессорам: SSE и AVX для архитектуры x86, MIPS SIMD [L15] для архитектуры MIPS.
MIPS SIMD
Теперь, когда мы понимаем основные принципы, на которых строятся векторные расширения архитектуры, можно перейти непосредственно к MIPS SIMD. Исчерпывающее описание этого расширения приведено в документации [D2], отметим основные моменты:
- доступно в двух вариантах: MIPS32 SIMD и MIPS64 SIMD. Так как Байкал-Т1 основан на ядрах архитектуры MIPS 32 r5, то в дальнейшем, упоминая MIPS SIMD, будем иметь в ввиду MIPS32 SIMD. На сайте и в документации Imagination Technologies оно также упоминается как MSA (MIPS SIMD Architecture);
- работа осуществляется с использованием 32x 128-битных регистров для обработки векторных данных. Каждый из которых может быть представлен как: 16x 8-битных векторов, 8x 16-битных, 4x 32-битных, либо 2x 64-битных:
- предполагает более 150 инструкций по обработке данных: целочисленных, с плавающей и фиксированной точкой, включая битовые операции, операции сравнения, конвертации:
целочисленные операции
Mnemonic | Instruction Description |
ADDV, ADDVI | Add |
ADD_A, ADDS_A | Add and Saturated Add Absolute Values |
ADDS_S, ADDS_U | Signed and Unsigned Saturated Add |
HADD_S, HADD_U | Signed and Unsigned Horizontal Add |
ASUB_S, ASUB_U | Absolute Value of Signed and Unsigned Subtract |
AVE_S, AVE_U | Signed and Unsigned Average |
AVER_S, AVER_U | Signed and Unsigned Average with Rounding |
DOTP_S, DOTP_U | Signed and Unsigned Dot Product |
DPADD_S, DPADD_U | Signed and Unsigned Dot Product Add |
DPSUB_S, DPSUB_U | Signed and Unsigned Dot Product Subtract |
DIV_S, DIV_U | Divide |
MADDV | Multiply-Add |
MAX_A, MIN_A | Maximum and Minimum of Absolute Values |
MAX_S, MAXI_S, MAX_U, MAXI_U | Signed and Unsigned Maximum |
MIN_S, MINI_S, MIN_U, MINI_U | Signed and Unsigned Maximum |
MSUBV | Multiply-Subtract |
MULV | Multiply |
MOD_S, MOD_U | Signed and Unsigned Remainder (Modulo) |
SAT_S, SAT_U | Signed and Unsigned Saturate |
SUBS_S, SUBS_U | Signed and Unsigned Saturated Subtract |
HSUB_S, HSUB_U | Signed and Unsigned Horizontal Subtract |
SUBSUU_S | Signed Saturated Unsigned Subtract |
SUBSUS_U | Unsigned Saturated Signed Subtract from Unsigned |
SUBV, SUBVI | Subtract |
битовые операции
Mnemonic | Instruction Description |
AND, ANDI | Logical And |
BCLR, BCLRI | Bit Clear |
BINSL, BINSLI, BINSR, BINSRI | Bit Insert Left and Right |
BMNZ, BMNZI | Bit Move If Not Zero |
BMZ, BMZI | Bit Move If Zero |
BNEG, BNEGI | Bit Negate |
BSEL, BSELI | Bit Select |
BSET, BSETI | Bit Set |
NLOC Leading | One Bits Count |
NLZC Leading | Zero Bits Count |
NOR, NORI | Logical Negated Or |
PCNT | Population (Bits Set to 1) Count |
OR, ORI | Logical Or |
SLL, SLLI | Shift Left |
SRA, SRAI | Shift Right Arithmetic |
SRAR, SRARI | Rounding Shift Right Arithmetic |
SRL, SRLI | Shift Right Logical |
SRLR, SRLRI | Rounding Shift Right Logical |
XOR, XORI | Logical Exclusive Or |
арифметические операции с плавающей точкой
Mnemonic | Instruction Description |
FADD | Floating-Point Addition |
FDIV | Floating-Point Division |
FEXP2 | Floating-Point Base 2 Exponentiation |
FLOG2 | Floating-Point Base 2 Logarithm |
FMADD, FMSUB | Floating-Point Fused Multiply-Add and Multiply-Subtract |
FMAX, FMIN | Floating-Point Maximum and Minimum |
FMAX_A, FMIN_A | Floating-Point Maximum and Minimum of Absolute Values |
FMUL | Floating-Point Multiplication |
FRCP | Approximate Floating-Point Reciprocal |
FRINT | Floating-Point Round to Integer |
FRSQRT | Approximate Floating-Point Reciprocal of Square Root |
FSQRT | Floating-Point Square Root |
FSUB | Floating-Point Subtraction |
не арифметические операции с плавающей точкой
Mnemonic | Instruction Description |
FCLASS | Floating-Point Class Mask |
операции сравнения с плавающей точкой
Mnemonic | Instruction Description |
FCAF | Floating-Point Quiet Compare Always False |
FCUN | Floating-Point Quiet Compare Unordered |
FCOR | Floating-Point Quiet Compare Ordered |
FCEQ | Floating-Point Quiet Compare Equal |
FCUNE | Floating-Point Quiet Compare Unordered or Not Equal |
FCUEQ | Floating-Point Quiet Compare Unordered or Equal |
FCNE | Floating-Point Quiet Compare Not Equal |
FCLT | Floating-Point Quiet Compare Less Than |
FCULT | Floating-Point Quiet Compare Unordered or Less Than |
FCLE | Floating-Point Quiet Compare Less Than or Equal |
FCULE | Floating-Point Quiet Compare Unordered or Less Than or Equal |
FSAF | Floating-Point Signaling Compare Always False |
FSUN | Floating-Point Signaling Compare Unordered |
FSOR | Floating-Point Signaling Compare Ordered |
FSEQ | Floating-Point Signaling Compare Equal |
FSUNE | Floating-Point Signaling Compare Unordered or Not Equal |
FSUEQ | Floating-Point Signaling Compare Unordered or Equal |
FSNE | Floating-Point Signaling Compare Not Equal |
FSLT | Floating-Point Signaling Compare Less Than |
FSULT | Floating-Point Signaling Compare Unordered or Less Than |
FSLE | Floating-Point Signaling Compare Less Than or Equal |
FSULE | Floating-Point Signaling Compare Unordered or Less Than or Equal |
операции преобразования с плавающей точкой
Mnemonic | Instruction Description |
FEXDO | Floating-Point Down-Convert Interchange Format |
FEXUPL, FEXUPR | Left-Half and Right-Half Floating-Point Up-Convert Interchange Format |
FFINT_S, FFINT_U | Floating-Point Convert from Signed and Unsigned Integer |
FFQL, FFQR | Left-Half and Right-Half Floating-Point Convert from Fixed-Point |
FTINT_S, FTINT_U | Floating-Point Round and Convert to Signed and Unsigned Integer |
FTRUNC_S, FTRUNC_U | Floating-Point Truncate and Convert to Signed and Unsigned Integer |
FTQ | Floating-Point Round and Convert to Fixed-Point |
операции с фиксированной точкой
Mnemonic | Instruction Description |
MADD_Q, MADDR_Q | Fixed-Point Multiply and Add without and with Rounding |
MSUB_Q, MSUBR_Q | Fixed-Point Multiply and Subtract without and with Rounding |
MUL_Q, MULR_Q | Fixed-Point Multiply without and with Rounding |
операции ветвления
Mnemonic | Instruction Description |
BNZ | Branch If Not Zero |
BZ | Branch If Zero |
CEQ, CEQI | Compare Equal |
CLE_S, CLEI_S, CLE_U, CLEI_U | Compare Less-Than-or-Equal Signed and Unsigned |
CLT_S, CLTI_S, CLT_U, CLTI_U | Compare Less-Than Signed and Unsigned |
операции загрузки и выгрузки векторов
Mnemonic | Instruction Description |
CFCMSA, CTCMSA | Copy from and copy to MSA Control Register |
LD | Load Vector |
LDI | Load Immediate |
MOVE | Vector to Vector Move |
SPLAT, SPLATI | Replicate Vector Element |
FILL Fill | Vector from GPR |
INSERT, INSVE | Insert GPR and Vector element 0 to Vector Element |
COPY_S, COPY_U | Copy element to GPR Signed and Unsigned |
ST | Store Vector |
операции перестановки элементов вектора
Mnemonic | Instruction Description |
ILVEV, ILVOD | Interleave Even, Odd |
ILVL, ILVR | Interleave the Left, Right |
PCKEV, PCKOD | Pack Even and Odd Elements |
SHF | Set Shuffle |
SLD, SLDI | Element Slide |
VSHF | Vector shuffle |
иные операции
Mnemonic | Instruction Description |
LSA | Left-shift add or load/store address calculation |
- большинство команд доступно в нескольких вариантах, отличающихся друг от друга размером элемента вектора и типом обрабатываемых данных. Так уже знакомая нам операция умножения с накоплением реализована как: FMADD — для чисел с плавающей точкой, MADD_Q — для чисел с фиксированной точкой (с насыщением), MADDR_Q — для чисел с фиксированной точкой (с насыщением и округлением), MADDV — для целых чисел. При этом, к примеру, MADDV доступна в 4х вариантах: MADDV.B — когда элементами для побайтовой обработки (x8), MADDV.H — когда каждый элемент вектора является полусловом (x16), MADDV.W — словом (x32) и MADDV.D — двойным словом (x64). Формальное описание этой команды в документации выглядит следующим образом:
Поддержка на уровне компилятора
MIPS SIMD поддерживается компилятором gcc, вместе с тем у этой поддержки есть свои особенности:
- для элементов вектора различной размерности введены отдельные типы данных [L16];
- мы избавлены от необходимости непосредственной работы с регистрами (работа ведется на уровне переменных соответствующего типа);
- для всех перечисленных выше операций введены высокоуровневые псевдонимы [L17];
- согласно документации gcc, компилятор не принимает самостоятельных решений об использовании MIPS SIMD, это же подтверждается неплохо разобранным примером применения MIPS SIMD, опубликованном на сайте Imagination Technologies [D3]. Таким образом, если провести грубую аналогию, то работа с MIPS SIMD ближе к написанию ассемблерных вставок, нежели к высокоуровневому программированию. Что должно учитываться при принятии решения об оптимизации.
В упомянутом выше примере приводится следующий код до оптимизации:
#define ROUND_POWER_OF_TWO(value, n) (((value) + (1 << ((n) - 1))) >> (n))
static inline unsigned char clip_pixel(int i32Val)
{
return ((i32Val) > 255) ? 255u : ((i32Val) < 0) ? 0u : (i32Val);
}
void vert_filter_8taps_16width_c(unsigned char *pSrc, // SOURCE POINTER
int SrcStride, // SOURCE BUFFER PITCH
unsigned char *pDst, // DEST POINTER
int DstStride, // DEST BUFFER PITCH
char *pFilter, // POINTER TO FILTER BANK
int Height) // HEIGHT OF THE BLOCK
{
unsigned int Row, Col;
int FiltSum;
short Src0, Src1, Src2, Src3, Src4, Src5, Src6, Src7;
pSrc -= (8 / 2 - 1) * SrcStride; // MOVE INPUT SRC POINTER TO APPROPRIATE POSITION
// LOOP FOR NUMBER OF COLUMNS-16
for (Col = 0; Col < 16; ++Col)
{
Src0 = pSrc[0 * SrcStride];
Src1 = pSrc[1 * SrcStride];
Src2 = pSrc[2 * SrcStride];
Src3 = pSrc[3 * SrcStride];
Src4 = pSrc[4 * SrcStride];
Src5 = pSrc[5 * SrcStride];
Src6 = pSrc[6 * SrcStride];
// LOOP FOR NUMBER OF ROWS
for (Row = 0; Row < Height; Row++)
{
Src7 = pSrc[(7 + Row) * SrcStride];
FiltSum = 0;
// ACCUMULATED FILTER SUM += PIXEL * FILTER COEFF
FiltSum += (Src0 * pi8Filter[0]);
FiltSum += (Src1 * pi8Filter[1]);
FiltSum += (Src2 * pi8Filter[2]);
FiltSum += (Src3 * pi8Filter[3]);
FiltSum += (Src4 * pi8Filter[4]);
FiltSum += (Src5 * pi8Filter[5]);
FiltSum += (Src6 * pi8Filter[6]);
FiltSum += (Src7 * pi8Filter[7]);
FiltSum = ROUND_POWER_OF_TWO(FiltSum, 7); // ROUNDING
pDst[Row * DstStride] = clip_pixel(FiltSum);// CLIP RESULT IN 0-255(UNSIGNED CHAR)
// PREPARING FOR NEXT CONVOLUTION- SLIDING WINDOW
Src0 = Src1;
Src1 = Src2;
Src2 = Src3;
Src3 = Src4;
Src4 = Src5;
Src5 = Src6;
Src6 = Src7;
}
pSrc += 1;
pDst += 1;
}
}
И он же после оптимизации с использованием MIPS SIMD:
/* MSA VECTOR TYPES */
#define WRLEN 128 // VECTOR REGISTER LENGTH 128-BIT
#define NUMWRELEM (WRLEN >> 3)
typedef signed char IMG_VINT8 __attribute__ ((vector_size(NUMWRELEM))); //VEC SIGNED BYTES
typedef unsigned char IMG_VUINT8 __attribute__ ((vector_size(NUMWRELEM))); //VEC UNSIGNED BYTES
typedef short IMG_VINT16 __attribute__ ((vector_size(NUMWRELEM))); //VEC SIGNED HALF-WORD
#define LOAD_UNPACK_VEC(pSrc, SrcStride, vi16VecRight, vi16VecLeft) \
{ \
IMG_VUINT8 vu8Src; \
IMG_VINT16 vi16Vec0; \
IMG_VINT8 vi8Tmp0; \
/* LOAD INPUT VECTOR */ \
vu8Src = *((IMG_VINT8 *)(pSrc)); \
/* RANGE WARPING TO MAINTAIN 16 BIT PRECISION */ \
vi16Vec0 = __builtin_msa_xori_b(vu8Src, 128); \
/* CALCULATE SIGN EXTENSION */ \
vi8Tmp0 = __builtin_msa_clti_s_b(vi16Vec0, 0); \
/* INTERLEAVE RIGHT TO 16 BIT VEC */ \
vi16VecRight = __builtin_msa_ilvr_b(vi8Tmp0, vi16Vec0); \
/* INTERLEAVE LEFT TO 16 BIT VEC */ \
vi16VecLeft = __builtin_msa_ilvl_b(vi8Tmp0, vi16Vec0); \
pSrc += SrcStride; \
}
void vert_filter_8taps_16width_msa(unsigned char *pSrc, // SOURCE POINTER
int SrcStride, // SOURCE BUFFER PITCH
unsigned char *pDst, // DEST POINTER
int DstStride, // DEST BUFFER PITH
char *pFilter, // POINTER TO FILTER BANK
int Height) // HEIGHT OF THE BLOCK
{
int u32LoopCnt;
VINT16 vi16Vec0Right, vi16Vec1Right, vi16Vec2Right, vi16Vec3Right;
VINT16 vi16Vec4Right, vi16Vec5Right, vi16Vec6Right, vi16Vec7Right;
VINT16 vi16Vec0Left, vi16Vec1Left, vi16Vec2Left, vi16Vec3Left;
VINT16 vi16Vec4Left, vi16Vec5Left, vi16Vec6Left, vi16Vec7Left;
VINT16 vi16Temp1Right, vi16Temp1Left;
VINT16 vi16Filt0, vi16Filt1, vi16Filt2, vi16Filt3;
VINT16 vi16Filt4, vi16Filt5, vi16Filt6, vi16Filt7;
pSrc -= (3 * SrcStride);
// PREPARE FILTER COEFF IN VEC REGISTERS
vi16Filt0 = __builtin_msa_fill_h(*(pFilter));
vi16Filt1 = __builtin_msa_fill_h(*(pFilter + 1));
vi16Filt2 = __builtin_msa_fill_h(*(pFilter + 2));
vi16Filt3 = __builtin_msa_fill_h(*(pFilter + 3));
vi16Filt4 = __builtin_msa_fill_h(*(pFilter + 4));
vi16Filt5 = __builtin_msa_fill_h(*(pFilter + 5));
vi16Filt6 = __builtin_msa_fill_h(*(pFilter + 6));
vi16Filt7 = __builtin_msa_fill_h(*(pFilter + 7));
//LOAD 7 INPUT VECTORS
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec0Right, vi16Vec0Left)
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec1Right, vi16Vec1Left)
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec2Right, vi16Vec2Left)
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec3Right, vi16Vec3Left)
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec4Right, vi16Vec4Left)
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec5Right, vi16Vec5Left)
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec6Right, vi16Vec6Left)
// START CONVOLUTION VERTICALLY
for (u32LoopCnt = Height; u32LoopCnt--; )
{
//LOAD 8TH INPUT VECTOR
LOAD_UNPACK_VEC(pSrc, SrcStride, vi16Vec7Right, vi16Vec7Left)
/* FILTER CALC */
IMG_VINT16 vi16Tmp1, vi16Tmp2;
IMG_VINT8 vi8Tmp3;
// 8 TAP VECTORIZED CONVOLUTION FOR RIGHT HALF
vi16Tmp1 = (vi16Vec0Right * vi16Filt0);
vi16Tmp1 += (vi16Vec1Right * vi16Filt1);
vi16Tmp1 += (vi16Vec2Right * vi16Filt2);
vi16Tmp1 += (vi16Vec3Right * vi16Filt3);
vi16Tmp2 = (vi16Vec4Right * vi16Filt4);
vi16Tmp2 += (vi16Vec5Right * vi16Filt5);
vi16Tmp2 += (vi16Vec6Right * vi16Filt6);
vi16Tmp2 += (vi16Vec7Right * vi16Filt7);
vi16Temp1Right = __builtin_msa_adds_s_h(vi16Tmp1, vi16Tmp2);
// 8 TAP VECTORIZED CONVOLUTION FOR LEFT HALF
vi16Tmp1 = (vi16Vec0Left * vi16Filt0);
vi16Tmp1 += (vi16Vec1Left * vi16Filt1);
vi16Tmp1 += (vi16Vec2Left * vi16Filt2);
vi16Tmp1 += (vi16Vec3Left * vi16Filt3);
vi16Tmp2 = (vi16Vec4Left * vi16Filt4);
vi16Tmp2 += (vi16Vec5Left * vi16Filt5);
vi16Tmp2 += (vi16Vec6Left * vi16Filt6);
vi16Tmp2 += (vi16Vec7Left * vi16Filt7);
vi16Temp1Left = __builtin_msa_adds_s_h(vi16Tmp1, vi16Tmp2);
// ROUNDING RIGHT SHIFT RANGE CLIPPING AND NARROWING
vi16Temp1Right = __builtin_msa_srari_h(vi16Temp1Right, 7);
vi16Temp1Right = __builtin_msa_sat_s_h(vi16Temp1Right, 7);
vi16Temp1Left = __builtin_msa_srari_h(vi16Temp1Left, 7);
vi16Temp1Left = __builtin_msa_sat_s_h(vi16Temp1Left, 7);
vi8Tmp3 = __builtin_msa_pckev_b(vi16Temp1Left, vi16Temp1Right);
vi8Tmp3 = __builtin_msa_xori_b(vi8Tmp3, 128);
// STORE OUTPUT VEC
*((IMG_VINT8 *)(pDst)) = (vi8Tmp3);
pDst += DstStride;
// PREPARING FOR NEXT CONVOLUTION- SLIDING WINDOW
vi16Vec0Right = vi16Vec1Right;
vi16Vec1Right = vi16Vec2Right;
vi16Vec2Right = vi16Vec3Right;
vi16Vec3Right = vi16Vec4Right;
vi16Vec4Right = vi16Vec5Right;
vi16Vec5Right = vi16Vec6Right;
vi16Vec6Right = vi16Vec7Right;
vi16Vec0Left = vi16Vec1Left;
vi16Vec1Left = vi16Vec2Left;
vi16Vec2Left = vi16Vec3Left;
vi16Vec3Left = vi16Vec4Left;
vi16Vec4Left = vi16Vec5Left;
vi16Vec5Left = vi16Vec6Left;
vi16Vec6Left = vi16Vec7Left;
}
}
Оценка производительности
Первоначально у меня были мысли написать простейшее приложение (синтетический тест) для оценки прироста производительности при использовании MIPS SIMD. Но несмотря на всю свою привлекательность, этот вариант не является показательным, ввиду его оторванности от реальных задач пользователя. На наше счастье работники Imagination Technologies и MIPS сделали весомый вклад в ffmpeg [L18] — широко используемое open source приложение, предназначенное для конвертации аудио и видео [L19]. Полагаю, что они как никто другой знают, как правильно использовать рассматриваемую технологию, а значит этот код должен быть максимально эффективен.
Таким образом, если мы скомпилируем ffmpeg в двух вариантах: с поддержкой MIPS SIMD и без нее, то можно будет сравнить скорость работы на одинаковых входных данных и по результатам сделать некий вывод об эффективности векторных вычислений.
Сборка ffmpeg
Выполняется на машине x86 под управлением ОС Linux. Используются средства разработки с сайта Imagination Technologies [L20] в режиме кросс-компиляции. Тесты выполняются на последнем стабильном релизе на момент написания статьи — ffmpeg 3.3 [L19].
Конфигурация ffmpeg для варианта с поддержкой MIPS SIMD:
./configure --enable-cross-compile --prefix=../ffmpeg-msa
--cross-prefix=/home/stas/mipsfpga/toolchain/mips-mti-linux-gnu/2016.05-03/bin/mips-mti-linux-gnu-
--arch=mips --cpu=p5600 --target-os=linux --extra-cflags="-EL -static" --extra-ldflags="-EL -static"
--disable-iconv
И с отключенной поддержкой MIPS SIMD:
./configure --enable-cross-compile --prefix=../ffmpeg-soft
--cross-prefix=/home/stas/mipsfpga/toolchain/mips-mti-linux-gnu/2016.05-03/bin/mips-mti-linux-gnu-
--arch=mips --cpu=p5600 --target-os=linux --extra-cflags="-EL -static" --extra-ldflags="-EL -static"
--disable-iconv --disable-msa
Краткое описание настроек конфигурации:
Параметр | Описание |
---|---|
—enable-cross-compile | сборка будет осуществляться на машине с архитектурой, отличной отцелевой |
—prefix=../ffmpeg-msa | каталог, в который будут помещены файлы после команды make install |
—cross-prefix=../mips-mti-linux-gnu- | путь к toolchain |
—arch=mips | целевая архитектура — MIPS |
—cpu=p5600 | целевое процессорное ядро — p5600 |
—target-os=linux | целевая ОС — Linux |
—extra-cflags=»-EL -static» | целевая система — little endian, использовать статическоесвязывание |
—extra-ldflags=»-EL -static» | аналогично |
—disable-iconv | отключить функциональность, связанную с кодировками текста |
—disable-msa | не использовать MIPS SIMD |
Если вы планируете повторить эти действия, то учтите, что сборка ffmpeg 3.3 с поддержкой MIPS SIMD вываливается с мелкой ошибкой, для устранения которой необходимо в файл libavcodec\mips\hevcpred_msa.c добавить:
#include "libavcodec/hevcdec.h"
После завершения конфигурации выполняем сборку (make) и развертывание в заданный каталог (make install). В результате этих действий в каталоге ../ffmpeg-msa расположены исполняемые файлы ffmpeg с поддержкой MIPS SIMD, а в ../ffmpeg-soft — без этой поддержки.
Тестирование
Выполняется на процессоре Байкал-Т1:
# uname -a Linux baikal-BFK-18446744073709551615 4.4.41-bfk #0 SMP Tue Apr 25 15:54:24 MSK 2017 mips GNU/Linux
В качестве входных данных выступают два видеоролика закодированные с использованием x264 [L21] и x265 [L22]. Тестовой задачей является декодирование видео с получением скриншотов через равные промежутки времени:
./ffmpeg-mips/ffmpeg-msa/bin/ffmpeg -i ./The\ Simpsons\ Movie\ -\ Trailer_x264.mp4
-vf fps=1/10 ./out_img/ffmpeg-msa_x264_%d.jpg -report -benchmark
./ffmpeg-mips/ffmpeg-msa/bin/ffmpeg -i ./Tears_400_x265.mp4
-vf fps=1 ./out_img/ffmpeg-msa_x265_%d.jpg -report -benchmark
./ffmpeg-mips/ffmpeg-soft/bin/ffmpeg -i ./The\ Simpsons\ Movie\ -\ Trailer_x264.mp4
-vf fps=1/10 ./out_img/ffmpeg-soft_x264_%d.jpg -report -benchmark
./ffmpeg-mips/ffmpeg-soft/bin/ffmpeg -i ./Tears_400_x265.mp4
-vf fps=1 ./out_img/ffmpeg-soft_x265_%d.jpg -report -benchmark
Краткое описание опций запуска:
Параметр | Описание |
---|---|
-i ./Tears_400_x265.mp4 | файл, который подлежит обработке |
-vf fps=1 | период (частота) снятия скриншотов (1 сек — для короткого ролика, 10 сек — для длинного) |
./out_img/ffmpeg-softx264%d.jpg | шаблон имени выходного файла |
-report | сформировать отчет по результатам работы |
-benchmark | включить в отчет данные о производительности |
Результаты работы ffmpeg:
Сценарий | Длительность (cек) |
---|---|
декодирование x264 с поддержкой MIPS SIMD | 113 |
декодирование x265 с поддержкой MIPS SIMD | 22 |
декодирование x264 без MIPS SIMD | 164 |
декодирование x265 без MIPS SIMD | 52 |
Таким образом, реальное время декодирования при использовании MIPS SIMD сокращается в 1.5 — 2.4 раза
Полные логи работы ffmpeg опубликованы на github [L23]:
- декодирование x264 с поддержкой MIPS SIMD (отчет);
- декодирование x265 с поддержкой MIPS SIMD (отчет);
- декодирование x264 без MIPS SIMD (отчет);
- декодирование x265 без MIPS SIMD (отчет);
Выводы
- MIPS SIMD является достаточно мощным инструментом повышения производительности и позволяет в разы сократить реальное время вычислений и соответствующим образом увеличить их скорость;
- оптимизация — это дорого в части трудозатрат разработчика. Реализация какого-либо алгоритма с использованием MIPS SIMD ближе к написанию ассемблерных вставок, нежели к высокоуровневому программированию. Что требует взвешенного подхода при принятии решений о переработке кода.
- Байкал-Т1 — это не вымысел маркетологов, он существует, работает, под него вполне спокойно была собрана последняя версия достаточно сложного программного пакета и она на нем без проблем запустилась.
Ссылки
[L1] — Процессор Байкал-Т1;
[L2] — Проект MIPSfpga-plus на github;
[L3] — P-Class P5600 Multiprocessor Core;
[L4] — Добавляем инструкции в микропроцессор MIPS, которые работают в конвейере как его собственные;
[L5] — Texas Instruments. Digital Signal Processors;
[L6] — Альтернативное использование мощностей GPU;
[L7] — OpenCL. Что это такое и зачем он нужен;
[L8] — Wikipedia: Фильтр с конечной импульсной характеристикой;
[L9] — TFilter. Free online FIR filter design tool;
[L10] — Wikipedia: Цифровой сигнальный процессор;
[L11] — Wikipedia: Полосно-заграждающий фильтр;
[L12] — Wikipedia: Амплитудно-частотная характеристика;
[L13] — Wikipedia: SIMD;
[L14] — Wikipedia: Векторный процессор;
[L15] — MIPS SIMD;
[L16] — GCC: MIPS SIMD Architecture (MSA) Support;
[L17] — GCC: MIPS SIMD Architecture Built-in Functions
[L18] — Проект ffmpeg на github (каталог libavcodec/mips/);
[L19] — FFmpeg multimedia framework;
[L20] — Codescape MIPS SDK;
[L21] — H.264 Demo Clips;
[L22] — x256. Sample HEVC Video Files;
[L23] — Логи работы ffmpeg
[L24] — Материалы семинара Специализированные интегральные схемы наноуровня
Документация
[D1] — Титце У., Шенк К. — Полупроводниковая схемотехника;
[D2] — MIPS Architecture for Programmers Volume IV-j: The MIPS32 SIMD Architecture Module;
[D3] — MIPS SIMD programming. Optimizing multimedia codecs
Изображения
[P1] — Блок-схема Байкал-Т1 (Источник: L1);
[P2] — TFilter. Пример настройки режекторного фильтра 1 (скриншот);
[P3] — TFilter. АЧХ режекторного фильтра 1 (скриншот);
[P4] — TFilter. Пример настройки режекторного фильтра 2 (скриншот);
[P5] — TFilter. АЧХ режекторного фильтра 2 (скриншот);
[P6] — Сравнение традиционного подхода и SIMD-реализации фильтра (Источник: D3);
[P7] — MSA Vector registers (Источник: D2);
[P8] — MADDV Operation description (Источник: D2)