Технология 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)