В статье описывается подход к программированию многоядерных сигнальных процессоров на основе OpenMP. Рассматриваются директивы OpenMP, разбирается их смысл и варианты использования. Делается акцент на цифровых сигнальных процессорах. Примеры применения директив OpenMP выбраны приближенными к задачам цифровой обработки сигналов. Реализация проводится на процессоре TMS320C6678 фирмы Texas Instruments, включающем 8 DSP-ядер. В части I статьи рассматриваются основные директивы OpenMP. Во II части статьи планируется дополнить список директив, а также рассмотреть вопросы внутренней организации работы OpenMP и вопросы оптимизации программного обеспечения.

Данная статья отражает лекционно-практический материал, предлагаемый слушателям в рамках курсов повышения квалификации по программе «Многоядерные процессоры цифровой обработки сигналов C66x фирмы Texas Instruments», проводимых ежегодно в Рязанском радиотехническом университете. Статья планировалась к публикации в одном из научно-технических журналов, но в силу специфики рассматриваемых вопросов было принято решение о накоплении материала для учебного пособия по многоядерным DSP-процессорам. А пока данный материал будет копиться, он вполне может полежать на страницах Интернета в свободном доступе. Отзывы и пожелания приветствуются.

Введение


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

С точки зрения разработчика систем обработки сигналов реального времени, переход к использованию многоядерных архитектур цифровых сигнальных процессоров (ЦСП) можно выразить тремя основными проблемами. Первая – это освоение аппаратной платформы, ее возможностей, назначения тех или иных блоков и режимов их работы, заложенных производителем [1]. Вторая – адаптация алгоритма обработки и принципа организации системы для реализации на многоядерном ЦСП (МЦСП) [3]. Третья – разработка программного обеспечения (ПО) цифровой обработки сигналов, реализуемого на МЦСП. При этом разработка ПО для МЦСП имеет ряд принципиальных отличий от разработки традиционных одноядерных приложений, включая распределение тех или иных фрагментов кода по ядрам, разделение данных, синхронизацию ядер, обмен данными и служебной информацией между ядрами, синхронизацию кэш и другие.

Одним из наиболее привлекательных решений для портирования имеющегося «одноядерного» ПО на многоядерную платформу или для разработки новых «параллельных» программных продуктов является инструментарий Open Multi-Processing (OpenMP). OpenMP представляет собой набор директив компилятору, функций и переменных окружения, которые могут встраиваться в стандартные языки программирования, в первую очередь, в наиболее распространенный язык Си, расширяя его возможности организацией параллельных вычислений. Это основное достоинство OpenMP-подхода. Не нужно изобретения/изучения новых языков параллельного программирования. Одноядерная программа легко превращается в многоядерную путем добавления в стандартный код простых и понятных директив компилятору. Все что нужно, это чтобы компилятор данного процессора поддерживал OpenMP. То есть производители процессоров должны позаботиться о том, чтобы их компиляторы «понимали» директивы OpenMP-стандарта и переводили их в соответствующие ассемблерные коды.

Стандарт OpenMP разрабатывается ассоциацией нескольких крупных производителей вычислительной техники и регулируется организацией OpenMP Architecture Review Board (ARB) [4]. При этом он является универсальным, не предназначенным для конкретных аппаратных платформ конкретных производителей. Организация ARB открыто публикует спецификацию очередных версий стандарта [5]. Также представляет интерес краткий справочник по OpenMP [6].

В последнее время применению OpenMP в различных приложениях и на различных платформах посвящено огромное число работ [7-12]. Особый интерес представляют книги, позволяющие в полном объеме получить базовые знания по использованию OpenMP. В отечественной литературе это источники [13-16].

Данная работа посвящена описанию директив, функций и переменных окружения OpenMP. При этом спецификой работы является ее ориентация на задачи цифровой обработки сигналов. Примеры, иллюстрирующие смысл тех или иных директив, берутся с акцентом на реализацию на МЦСП. В качестве аппаратной платформы выбраны процессоры МЦСП TMS320C6678 фирмы Texas Instruments [17], включающие в свой состав 8 DSP-ядер. Данная платформа МЦСП является одной из передовых, пользующихся широким спросом на отечественном рынке. Кроме того, в работе рассматривается ряд вопросов внутренней организации механизмов OpenMP, имеющих значение для задач обработки сигналов реального времени, а также вопросы оптимизации.

Постановка задачи


Итак, пусть задача обработки состоит в формировании выходного сигнала, как суммы двух входных сигналов одинаковой длины:

z(n) = x(n) + y(n), n = 0, 1, …, N-1

«Одноядерная» реализация данной задачи на стандартном языке Си/Си++ может выглядеть следующим образом:

void vecsum(float * x, float * y, float * z, int N)
{
 for ( int i=0; i<N; i++)
    z[i] = x[i] + y[i];
}

Пусть теперь мы имеем 8-ядерный процессор TMS320C6678. Возникает вопрос, как задействовать возможности многоядерной архитектуры для реализации данной программы?

Одним из решений является разработка 8 отдельных программ и независимая загрузка их на 8 ядер. Это чревато наличием 8 отдельных проектов, в которых необходимо учитывать совместные правила исполнения: расположение массивов в памяти, разделение частей массивов между ядрами и прочее. Кроме того, необходимо будет написание дополнительных программ, выполняющих синхронизацию ядер: если одно ядро завершило формирование своей части массива, это еще не значит, что весь массив готов; необходимо или вручную проверять, завершение работы всех ядер, или пересылать со всех ядер флаги завершения обработки на одно «главное» ядро, которое будет выдавать соответствующее сообщение о готовности выходного массива.

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

Начальные настройки OpenMP


Перед началом использования OpenMP в своей программе, очевидно, необходимо подключить данный функционал к своему проекту. Для процессоров TMS320C6678 это означает модификацию файла конфигурации проекта и используемой платформы, а также включение в свойства проекта ссылок на компоненты OpenMP. Такие специфические для конкретной аппаратной платформы настройки мы не будем рассматривать в статье. Рассмотрим более общие начальные настройки OpenMP.

Поскольку OpenMP является расширением языка Си, включение его директив и функций в свою программу должно сопровождаться включением файла описания этого функционала:

#include <ti/omp/omp.h>

Далее необходимо сообщить компилятору (и функционалу OpenMP) с каким числом ядер мы имеем дело. Отметим, что OpenMP работает не с ядрами, а с параллельными потоками. Параллельный поток – понятие логическое, а ядро – физическое, аппаратное. В частности, на одном ядре могут реализовываться несколько параллельных потоков. В то же время, по-настоящему параллельное исполнения кода, естественно, подразумевает, что число параллельных потоков совпадает с числом ядер, и каждый поток реализуется на своем ядре. В дальнейшем мы будем считать, что ситуация именно так и выглядит. Однако следует иметь в виду, что номер параллельного потока и номер ядра его реализующего не обязательно должны совпадать!

К начальным настройкам OpenMP мы отнесем задание числа параллельных потоков с использованием следующей функции OpenMP:

omp_set_num_threads(8);

Мы задали число ядер (потоков) равным 8.

Директива parallel


Итак, мы хотим, чтобы код представленной выше программы исполнялся на 8 ядрах. С OpenMP для этого достаточно всего лишь добавить в код директиву parallel следующим образом:

#include <ti/omp/omp.h>
void vecsum (float * x, float * y, float * z, int N)
{

 omp_set_num_threads(8);
 #pragma omp parallel
    {
     for ( int i=0; i<N; i++)
        z[i] = x[i] + y[i];
    }
 }

Все директивы OpenMP оформляются в виде конструкций вида:

#pragma omp <имя_директивы> [опция[(,)][опция[(,)]] …].

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

Мы получили программу, которая выполняется на одном главном или ведущем ядре (master core), а те фрагменты, которые выделены директивой parallel выполняются на заданном числе ядер, включая как ведущее, так и ведомые ядра (slave core). В полученной реализации один и тот же цикл суммирования векторов будет выполняться сразу на 8 ядрах.

Типовая структура организации параллельных вычислений в OpenMP показана на рисунке 1.

image

Рисунок 1. Принцип организации параллельных вычислений в OpenMP

Исполнение программного кода всегда начинается с последовательного региона, исполняемого на одном ядре в ведущем потоке. В точке начала параллельного региона, обозначаемой соответствующей директивой OpenMP, происходит организация параллельного исполнения следующего за директивой OpenMP кода в наборе потоков (параллельный регион). На рисунке для упрощения показано только четыре параллельных потока. При завершении параллельного региона потоки объединяются, ожидая окончания работы друг друга, и далее вновь следует последовательный регион.

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

Проведем аналогию. Пусть есть бригада из 8 человек. Один из них является главным; остальные – его помощниками. К ним поступают запросы на проведение различных работ. Главный работник принимает и выполняет заказы, подключая, по возможности, своих помощников. Первая работа, за которую взялись наши работники, состояла в переводе текста с английского языка на русский. Бригадир взялся за выполнение работы, взял исходный текст, приготовил словари, скопировал текст для каждого из своих помощников и раздал всем один и тот же текст, не разделив работу между ними. Перевод будет выполнен. Задача будет решена корректно. Однако выигрыша от наличия 7 помощников не будет. Даже наоборот. Если им придется делить один и тот же словарь, или компьютер, или листок с исходным текстом, время выполнения задания может затянуться. Также работает и OpenMP в нашем первом примере. Требуется разделение работ. Каждому работнику следует указать, какой фрагмент общего текста должен переводить именно он.

Очевидным способом разделения работы между ядрами в контексте задачи суммирования массивов является распределение итераций цикла по ядрам в зависимости от номера ядра. Достаточно внутри параллельного региона узнать, на каком ядре выполняется код, и задать диапазон итераций цикла в зависимости от этого номера:

#include <ti/omp/omp.h>

void vecsum (float * x, float * y, float * z, int N)
{

omp_set_num_threads(8);
#pragma omp parallel
   {
     core_num = omp_get_thread_num();

     a=(N/8)*core_num;
     b=a+N/8;

     for (int  i=a; i<b; i++)
        z[i] = x[i] + y[i];
    }
}

Чтение номера ядра выполняется функцией OpenMP omp_get_thread_num();. Эта функция, находясь внутри параллельного региона, выполняется одинаково на всех ядрах, но на разных ядрах дает разный результат. За счет этого становится возможным дальнейшее разделение работы внутри параллельного региона. Для простоты мы считаем, что число итераций цикла N кратно числу ядер. Чтение номера ядра может быть основано аппаратно на наличии в каждом ядре специального регистра номера ядра – регистра DNUM на процессорах TMS320C6678. Обратиться к нему можно различными средствами, включая ассемблерные команды или функции библиотеки поддержки кристалла CSL. Однако можно воспользоваться и функционалом, предоставляемым надстройкой OpenMP. Здесь, однако, мы вновь должны обратить внимание на то, что номер ядра и номер параллельного региона OpenMP – это разные понятия. Например, 3-й параллельный поток вполне может исполняться на, скажем, 5-ом ядре. Более того, в следующем параллельном регионе или при повторном прохождении того же параллельного региона 3-ий поток может исполняться уже на, например, 4-ом ядре. И так далее.

Мы получили программу, выполняющуюся на 8 ядрах. Каждое ядро обрабатывает свою часть входных массивов и формирует соответствующую область выходного массива. Каждый из наших работников переводит свою 1/8 часть текста и в идеале мы получаем 8-кратное ускорение решения задачи.

Директивы for и parallel for


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

Автоматическое указание, как работа внутри параллельного региона делится между ядрами, возможно с использованием дополнительной директивы for. Данная директива используется внутри параллельного региона непосредственно перед циклами типа for и говорит о том, что итерации цикла должны быть распределены между ядрами. Директивы parallel и for могут использоваться раздельно:

#pragma omp parallel
#pragma omp for

А могут использоваться совместно в одной директиве для сокращения записи:

#pragma omp parallel for

Применение в нашем примере сложения массивов директивы parallel for приводит к следующему программному коду:

#include <ti/omp/omp.h>

void vecsum (float * x, float * y, float * z, int N)
{
 int i;

 omp_set_num_threads(8);

 #pragma omp parallel for
    for (i=0; i<N; i++)
       z[i] = x[i] + y[i];

 }

Если сравнить данную программу с исходной одноядерной реализацией, то мы увидим, что отличия минимальны. Мы всего лишь подключили заголовочный файл omp.h, установили число параллельных потоков и добавили одну строчку – директиву parallel for.

Замечание 1. Еще одним отличием, которое мы намеренно скрываем в наших рассуждениях, является перенос объявления переменной i из цикла в раздел описания переменных функции, а точнее из параллельного в последовательный регион кода. Сейчас еще рано пояснять данное действие, однако, оно является принципиальным и будет пояснено позже в разделе, касающемся опций private и shared.

Замечание 2. Мы говорим, что итерации цикла делятся между ядрами, однако, мы не говорим, как конкретно они делятся. Какие конкретно итерации цикла, на каком из ядер будут выполняться? OpenMP имеет возможности задавать правила распределения итераций по параллельным потокам, и мы рассмотрим эти возможности позже. Однако точно привязать конкретное ядро к конкретным итерациям можно только вручную рассмотренным ранее способом. Правда обычно такая привязка не является необходимой. В случае, если число итераций цикла не кратно числу ядер, распределение итераций по ядрам будет производиться так, чтобы нагрузка распределялась максимально равномерно.

Директивы sections и parallel sections


Разделение работы между ядрами может производиться либо на основе разделения данных, либо на основе разделения задач. Вспомним про нашу аналогию. Если все работники выполняют одно и то же – занимаются переводом текста, – но каждый переводит разный фрагмент текста, то это относится к первому типу разделения работы – разделению данных. Если же работники выполняют различные действия, например, один занимается переводом всего текста, другой ищет для него слова в словаре, третий набирает текст перевода и так далее, то это относится ко второму типу разделения работы – разделению задач. Рассмотренные нами директивы parallel и for позволяли разделять работу путем разделения данных. Разделение задач между ядрами позволяет выполнять директива sections, которая, как и в случае директивы for, может использоваться независимо от директивы parallel или совместно с ней для сокращения записи:

#pragma omp parallel
#pragma omp sections

и

#pragma omp parallel sections

В качестве примера мы приведем программу, в которой используются 3 ядра процессора, и каждое из ядер выполняет свой алгоритм обработки входного сигнала x:

#include <ti/omp/omp.h>

void sect_example (float* x)
 {
  omp_set_num_threads(3);

  #pragma omp parallel sections
     {

  #pragma omp section
     Algorithm1(x);

  #pragma omp section
     Algorithm2(x);

   #pragma omp section
     Algorithm3(x);

     }
  }

Опции shared, private и default


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

float x[N];
float y[N];

void dotp (void)
{
 int i;
 float sum;
	
 sum = 0;

 for (i=0; i<N; i++)
    sum = sum + x[i]*y[i];

}

Результат выполнения (для тестовых массивов из 16 элементов) оказался равным:

[TMS320C66x_0] sum = 331.0

Перейдем к параллельной реализации данной программы, используя директиву parallel for:

float x[N];
float y[N];

void dotp (void)
{
 int i;
 float sum;

 sum = 0;

  #pragmaomp parallel for
     {		
       for (i=0; i<N; i++)
          sum = sum + x[i]*y[i];
     }
}

Результат выполнения:

[TMS320C66x_0] sum= 6.0

Программа дает неверный результат! Почему?

Чтобы ответить на этот вопрос, необходимо разобраться, как связаны значения переменных в последовательном и параллельном регионах. Опишем логику работы OpenMP более подробно.
Функция dotp() начинает выполняться в виде последовательного региона на 0-м ядре процессора. При этом в памяти процессора организованы массивы x и y, а также переменные I и sum. При достижении директивы parallel в действие вступают служебные функции OpenMP, которые организуют последующую параллельную работу ядер. Происходит инициализация ядер, их синхронизация, подготовка данных и общий старт. Что происходит при этом с переменными и массивами?

Все объекты в OpenMP (переменные и массивы) можно разделить на общие (shared) и частные (private). Общие объекты размещаются в общей памяти и используются равноправно всеми ядрами внутри параллельного региона. Общие объекты совпадают с одноименными объектами последовательного региона. Они переходят из последовательного в параллельный регион и обратно без изменений, сохраняя свое значение. Доступ к таким объектам внутри параллельного региона осуществляется равноправно для всех ядер, и возможны конфликты общего доступа. В нашем примере массивы x и y, а также переменная sum, по умолчанию оказались общими. Получается, что все ядра используют одну и ту же переменную sum в качестве аккумулятора. В результате иногда складывается ситуация, при которой несколько ядер одновременно считывают одинаковое текущее значение аккумулятора, добавляют к нему свой частичный вклад и записывают новое значение в аккумулятор. При этом то ядро, которое делает запись последним, стирает результаты работы остальных ядер. Именно по этой причине наш пример дал неправильный результат.

Принцип работы с общими и частными переменными проиллюстрирован на рисунке 2.

image

Рисунок 2. Иллюстрация работы OpenMP с общими и частными переменными

Частные объекты представляют собой копии исходных объектов, создаваемые отдельно для каждого ядра. Эти копии создаются динамически при инициализации параллельного региона. В нашем примере переменная i как счетчик итераций цикла по умолчанию считается частной. При достижении директивы parallel в памяти процессора создается 8 копий (по числу параллельных потоков) этой переменной. Частные переменные размещаются в частной памяти каждого ядра (могут размещаться в локальной памяти, а могут и в общей, в зависимости от того, как мы их объявили и сконфигурировали память). Частные копии по умолчанию никак не связаны с исходными объектами последовательного региона. По умолчанию значения исходных объектов не передаются в параллельный регион. Какими являются частные копии объектов в начале выполнения параллельного региона, неизвестно. По окончании параллельного региона значения частных копий просто теряются, если не принять специальных мер к передаче этих значений в последовательный регион, о которых мы расскажем далее.

Чтобы явно указать компилятору, какие объекты следует считать частными, а какие общими, совместно с директивами OpenMP применяются опции shared и private. Список объектов, относящихся к общим или частным, указывается через запятую в скобках после соответствующей опции. В нашем случае переменные i и sum должны быть частными, а массивы x и y – общими. Поэтому мы будем использовать конструкцию вида:

#pragma omp parallel for private(i, sum) shared(x, y)

при открытии параллельного региона. Теперь у каждого ядра будет свой аккумулятор, и накопления будут идти независимо друг от друга. Дополнительно аккумуляторы надо теперь обнулить, так как исходное их значение неизвестно. Кроме того, возникает вопрос, как объединить частные результаты, полученные на каждом из ядер. Одним из вариантов является использование специального общего массива из 8 ячеек, в который каждое ядро поместит свой результат внутри параллельного региона, а после выхода из параллельного региона главное ядро суммирует элементы этого массива и сформирует окончательный результат. Мы получаем следующий код программы:

float x[N];
float y[N];
float z[8];

void dotp (void)
{
 int i, core_num;
 float sum;
 sum = 0;

 #pragma omp parallel private(i, sum, core_num) shared(x, y, z)
   {
     core_num = omp_get_thread_num();

     sum = 0;

     #pragma omp for
        for (i=0; i<N; i++)
           sum = sum + x[i]*y[i];

     z[core_num] = sum;
   }

   for (i=0; i<8; i++)
      sum = sum + z[i];
}

Результат выполнения:

[TMS320C66x_0] sum= 331.0

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

Интересно, что при указании в качестве частных объектов имен массивов OpenMP при инициализации параллельного региона поступает также как и с переменными – динамически создает частные копии этих массивов. Убедиться в этом можно проведя простой эксперимент: объявив массив через опцию private, вывести значения указателей на этот массив в последовательном и в параллельном регионах. Мы увидим 9 разных адресов (при числе ядер – 8).

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

Если для объекта явное указание его типа (общий/частный) в директиве открытия параллельного региона отсутствует, то OpenMP «действует» по определенным правилам, описанным в [5]. Неописанные объекты OpenMP относит к типу по умолчанию. Какой это будет тип, private или shared, определяется переменной среды, – одним из параметров работы OpenMP. Данный параметр может задаваться и меняться в процессе работы. Исключение составляют переменные, используемые в качестве счетчиков итераций циклов. Они по умолчанию считаются частными. Правда это правило действует только для директив типа for и parallel for, поэтому лучше обращать на эти переменные особое внимание.

В связи с этим полезным оказывается применение опции default. Данная опция позволяет указать те объекты, для которых будет действовать правило – тип по умолчанию. При этом, если в качестве параметра этой опции выбрать none, то это будет означать, что никакая переменная не может принимать тип по умолчанию, то есть требуется обязательное явное указание типа всех объектов, встречаемых в параллельном регионе:

#pragma omp parallel private(sum, core_num) shared(x, y, z) default(i)

или:

#pragma omp parallel private(i, sum, core_num) shared(x, y, z) default(none)

Опция reduction


В рассмотренном примере реализации скалярного произведения на 8 ядрах мы отметили один недостаток: объединение частных результатов работы ядер требует существенных доработок кода, что делает его громоздким и неудобным. В то же время концепция openMP подразумевает максимальную прозрачность перехода от одноядерной к многоядерной реализации и обратно. Упростить программу, рассмотренную в предыдущем разделе, позволяет применение опции reduction.

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

reduction (идентификатор : список объектов)

идентификатор – определяет, какую операцию объединения частных результатов требуется выполнить. Он же задает начальные значения переменных, которые представляют частные результаты.
список объектов – имена переменных, которые используются для формирования частных результатов работы ядер.

Все возможные варианты использования опции reduction, предусмотренные стандартом OpenMP на данный момент, приведены в Таблице 1.

Возможные идентификаторы операции: +, *, -, &, |, ^, &&, ||, max, min

Соответствующие начальные значения переменных: 0, 1, 0, 0, 0, 0, 1, 0, наименьшее значение для данного типа, наибольшее значение для данного типа.

В нашей программе скалярного произведения следует использовать опцию reduction c идентификатором «+» для переменной sum:

float x[N];
float y[N];

void dotp (void)
{
 int i;
 float sum;

 #pragma omp parallel for private(i) shared(x, y) reduction(+:sum)
    for (i=0; i<N; i++)
       sum += x[i]*y[i];
}

Результат выполнения:

[TMS320C66x_0] sum= 331.0

Программа дает верный результат и при этом очень компактно выглядит и включает лишь минимальные отличия от исходного «последовательного» кода!

Синхронизация в OpenMP


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

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

В OpenMP можно выделить два типа синхронизации: неявную и явную. Неявная синхронизация происходит автоматически в конце параллельных регионов, а также по окончании ряда директив, которые могут применяться внутри параллельных регионов, включая omp for, omp sections и так далее. При этом автоматически происходит и синхронизация кэш.

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

Директива barrier


Директива barrier записывается в виде:

#pragma omp barrier

и в явном виде устанавливает точку синхронизации параллельных потоков OpenMP внутри параллельного региона. Приведем следующий пример использования директивы:

#define CORE_NUM	8
float z[CORE_NUM];

void arr_proc(void)
{
 omp_set_num_threads(CORE_NUM);
 int  i, core_num;
float sum;

 #pragma omp parallel private(core_num, i, sum)
  {
    core_num=omp_get_thread_num();
    z[core_num]=core_num;

#pragma omp barrier

sum = 0;
for(i=0;i<CORE_NUM;i++)
sum=sum+z[i];

#pragma omp barrier

z[core_num]=sum;
}

for(i=0;i<CORE_NUM;i++)
   printf("z[%d] = %f\n", i, z[i]);

}

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

Директива critical


Директива critical записывается в виде:

#pragma omp critical [имя региона]

И выделяет фрагмент кода внутри параллельного региона, который может исполняться одновременно только одним ядром.

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

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

#define	CORE_NUM	8
#define 	N 		1000
#define 	M 		80

void crit_ex(void)
{
  int i, j;
  int A[N];
  int Z[N] = {0};

  omp_set_num_threads(CORE_NUM);

  #pragma omp parallel for private (A)

  for (i = 0; i < M; i++)
    {

      poc_A(A, N);

      #pragma omp critical
      for (j=0; j<N; j++)
            Z[j] = Z[j] + A[j];

    }
}

В данной программе в цикле М раз повторяется обработка (формирование) массива А и накопление результатов обработки в массиве Z. При переходе к многоядерной реализации итерации цикла обработки распределяются между 8 ядрами. При этом массив А обрабатывается как частный, то есть независимо на каждом ядре. Обработка может идти на всех ядрах параллельно, поскольку зависимостей между этими процедурами нет. При накоплении результаты работы всех ядер объединяются в общем массиве Z. Если не принять специальных мер по синхронизации ядер, то параллельные потоки будут обращаться к одному общему ресурсу и вносить ошибку в работу друг друга. Чтобы предотвратить ошибки, можно запретить параллельным потокам в этом месте выполняться параллельно. Первое ядро, завладевшее ресурсом (в данном случае фрагментом кода), будет владеть им полностью, пока не выполнит все действия. Остальные ядра будут ожидать освобождения ресурса в начале критической секции кода. Фактически, мы переходим к последовательной обработке внутри параллельного региона.

Заменим в нашем коде критическую секцию на следующую конструкцию.

  #pragma omp critical (Z1add)
      for (j=0; j<N; j++)
         Z1[j] = Z1[j] + A[j];

   #pragma omp critical (Z2mult)
      for (j=0; j<N; j++)
         Z2[j] = Z2[j] * A[j];

Теперь мы имеем две критических секции. В одной идет объединение результатов работы ядер путем суммирования; в другой – путем умножения. Обе секции могут выполняться одновременно только на одном ядре, однако, разные секции могут выполняться одновременно на разных ядрах. Если в конструкцию директивы critical внести имя региона, то ядру будет отказано в доступе к коду, только если другое ядро работает именно в этом регионе. Если имена регионам не присвоены, то ядро не сможет войти ни в один из критических регионов, если другое ядро работает с любым из них, даже если они никак не связаны между собой.

Директива atomic


Директива atomic записывается в виде:

#pragma omp atomic [read | write | update | capture]

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

#define	CORE_NUM	8
#define 	N 		1000
#define 	M 		80

void crit_ex(void)
{
int i, j;
int A[N];
int Z[N] = {0};

omp_set_num_threads(CORE_NUM);

#pragma omp parallel for private (A)

for (i = 0; i < M; i++)
 {

   poc_A(A, N);

   for (j=0; j<N; j++)
      {
        #pragma omp atomic update
            Z[j] = Z[j] + A[j];
      }
}

Теоретически, применение директивы atomic должно существенно сократить время обработки, поскольку от полностью последовательного выполнения цикла мы переходим к последовательному выполнению только отдельных операций обращения к памяти, когда номера запрашиваемых элементов массива совпадают для разных ядер. Однако на практике эффективность данной идеи будет зависеть от способа ее реализации. Если, например, синхронизация ядер с помощью директивы atomic сводится к чтению флага, расположенного в общей памяти, на каждой итерации цикла, то время выполнения цикла может существенно возрастать. Другими словами, в случае директивы critical время выполнения цикла составит MxT1 тактов процессора, где М – число ядер, а T1 – время выполнения цикла одним ядром; а в случае директивы atomic время выполнения цикла составит Т2 тактов процессора. При этом цикл с директивой atomic включает дополнительный код синхронизации, и время Т2 может оказываться более, чем в М раз больше времени Т1.

Заключительные замечания


В данной статье мы рассмотрели основные конструкции OpenMP – расширения языков программирования высокого уровня (Си/Си++), используемого для автоматического, выполняемого компилятором, распараллеливания программного обеспечения для реализации на многоядерных процессорах. Особенностью данной статьи является ориентация на системы цифровой обработки сигналов и иллюстрация выполнения примеров программ на 8-ядерном ЦСП TMS320C6678 фирмы Texas Instruments. Основное достоинство OpenMP – это простота перехода от одноядерной к многоядерной реализации. Все задачи по взаимодействию ядер, включая обмен данными и синхронизацию, выполняют стандартные функции OpenMP, подключаемые на этапе компиляции. Однако удобство разработки обычно чревато меньшей эффективностью получаемого решения. Вопросы издержек на инструментарий OpenMP не рассматриваются в рамках данной статьи. Планируется посвятить этому отдельную работу.

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

Также следует отметить, что стандарт OpenMP закладывает общую идеологию. Эффективность же OpenMP зависит от реализации функций OpenMP для конкретной платформы процессоров. Так версии OpenMP 1 и 2, разработанные фирмой Texas Instruments для процессоров TMS320C6678 существенно отличаются. Вторая версия задействует многочисленные аппаратные механизмы ускорения взаимодействия ядер и оказывается существенно эффективнее первой версии. В последующих работах планируется раскрыть основные механизмы реализации функций OpenMP; провести анализ издержек, сопутствующих этим функциям; сформировать тестовые оценки времени реализации директив OpenMP; сформировать советы по повышению эффективности использования данного механизма.

Литература
1. G. Blake, R.G. Dreslinski, T. Mudge, «A survey of multicore processors,» Signal Processing Magazine, vol. 26, no. 6, pp. 26-37, Nov. 2009.
2. L.J. Karam, I. AlKamal, A. Gatherer, G.A. Frantz, «Trends in multicore DSP platforms,» Signal Processing Magazine, vol. 26, no. 6, pp. 38-49, 2009.
3. A. Jain, R. Shankar. Software Decomposition for Multicore Architectures, Dept. of Computer Science and Engineering, Florida Atlantic University, Boca Raton, FL, 33431.
4. Web-сайт OpenMP Architecture Review Board (ARB): openmp.org.
5. OpenMP Application Programming Interface. Version 4.5 November 2015. OpenMP Architecture Review Board. P. 368.
6. OpenMP 4.5 API C/C++ Syntax Reference Guide. OpenMP Architecture Review Board. 2015.
7. J. Diaz, C. Munoz-Caro, A. Nino. A Survey of Parallel Programming Models and Tools in the Multi and Many-Core Era. IEEE Transactions on Parallel and Distributed Systems. – 2012. – Vol. 23, Is. 8, pp. 1369 – 1386.
8. A. Cilardo, L. Gallo, A. Mazzeo, N. Mazzocca. Efficient and scalable OpenMP-based system-level design. Design, Automation & Test in Europe Conference & Exhibition (DATE). – 2013, pp. 988 – 991.
9. M. Chavarrias, F. Pescador, M. Garrido, A. Sanchez, C. Sanz. Design of multicore HEVC decoders using actor-based dataflow models and OpenMP. IEEE Transactions on Consumer Electronics. – 2016. – Vol. 62. – Is. 3, pp. 325 – 333.
10. M. Sever, E. Cavus. Parallelizing LDPC Decoding Using OpenMP on Multicore Digital Signal Processors. 45th International Conference on Parallel Processing Workshops (ICPPW). – 2016, pp. 46 – 51.
11. A. Kharin, S. Vityazev, V. Vityazev, N. Dahnoun. Parallel FFT implementation on TMS320c66x multicore DSP. 6th European Embedded Design in Education and Research Conference (EDERC). – 2014, pp. 46 – 49.
12. D. Wang, M. Ali, ?Synthetic Aperture Radar on Low Power Multi-Core Digital Signal Processor,? High Performance Extreme Computing (HPEC), IEEE Conference on, pp. 1 – 6, 2012.
13. В. П. Гергель, А. А. Лабутина. Учебно-образовательный комплекс по методам параллельного программирования. Н.Новгород, 2007, 138 с.
14. А. В. Сысоев. Высокопроизводительные вычисления в учебном процессе и научных исследованиях. Н. Новгород, 2006, 90 с.
15. А.С. Антонов. Параллельное программирование с использованием технологии OpenMP. Издательство Московского университета. 2009 г, 78 с.
16. М.П. Левин. Параллельное программирование с использованием OpenMP. М.: 2012, 121 с.
17. TMS320C6678 Multicore Fixed and Floating-Point Digital Signal Processor, Datasheet, SPRS691E, Texas Instruments, p. 248, 2014.
Поделиться с друзьями
-->

Комментарии (25)


  1. KonstantinSpb
    29.12.2016 18:34
    +1

    Напоминает map and reduce в Hadoop. То что в проце ядро, то в Hadoop отдельная нода в кластере


  1. R6MF49T2
    30.12.2016 01:28

    #include <ti/omp/omp.h>

    void vecsum (float * x, float * y, float * z, int N)
    {
    int i;

    omp_set_num_threads(8);

    #pragma omp parallel for
    for (i=a; i<b; i++)
    z[i] = x[i] + y[i];

    }

    Не ясно откуда в этом примере взялись переменные a и b и чему они равны.


    1. vsv630
      30.12.2016 09:23

      Спасибо за комментарий. Это была ошибка, причем достаточно принципиальная. Недосмотрели. В этом примере a и b быть не должно. Исправил. Более сложно подкорректировать предыдущий пример, где a и b задаются, но не объявлены. Дело в том, что их нужно передавать в параллельный регион как частные переменные, а в данном разделе (директива parallel) про частные переменные речи еще не было, и не хотелось бы забегать вперед. Как выйти из этой ситуации, подумаю…


  1. unabl4
    30.12.2016 02:28

    Спасибо за статью. Чем-то сильно напоминает CUDA программирование.


  1. serjeant
    30.12.2016 09:32

    Спасибо за публикацию!!!
    Я тоже занимаюсь программирование TMS320C6678. Было бы интересно познакомиться с примером применением OpenMP в конкретномм проекте, с описанием всех настроек и конфигурации проекта.
    В целом библиотека очень хорошая, но… Для выполнения коротких вычислений не всегда оправдана, поскольку много процессорного времени уходит на подготовку для распараллеливания, так же тяжело обрабатываются многоуровневые вложенные циклы. И использование OpenMP для TMS320С6678 навязывает использование дополнительных библиотек таких как IPC, SYS/BIOS и т.д. что приводит к раздуванию размера прошивки. По-этому иногда выгодней использовать оптимизацию и особые процессорные вычислительные функции чем данную библиотеку.
    Вы упомянули о курсах повышения квалификации «Многоядерные процессоры цифровой обработки сигналов C66x фирмы Texas Instruments». А есть возможность познакомиться с материалами данного курса?
    И очень-очень интересует учебное пособие по многоядерным DSP-процессорам.


    1. vsv630
      30.12.2016 09:41

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


      1. vsv630
        30.12.2016 09:50

        Подробнее о курсах повышения квалификации: www.dspa.ru/workshops/ws.php


        1. serjeant
          30.12.2016 11:34

          Благодарю за информацию, буду упрашивать начальство о выделении денег )))
          Преподаватель иностранец, а перевод его лекций будет?? Или как будет построено обучение?


    1. vsv630
      30.12.2016 09:46

      По поводу затрат на OpenMP — да они существенны. Это отдельная тема отдельной статьи. Важно отметить, что OpenMP 2-ой версии от TI СУЩЕСТВЕННО лучше, чем OpenMP 1-ой версии за счет применения Multicore Navigatora и других приемов. Мы пытались распараллеливать КИХ-фильтр и другие типовые задачи ЦОС, но C66x — это машина, которая рассчитана не на такие примитивные вычисления, а на более серьезные задачи. А в более серьезных задачах уровень распараллеливания гораздо выше и затраты на OpenMP становятся оправданными. В целом, считаю данный путь перспективным.


  1. telhin
    30.12.2016 11:01

    Сам работаю с TMS320C6678. В проекте используются OpenMP, NDK, FFTlib с поддержкой OpenMP. Разговоры о применении стандарта параллельных вычислений это конечно хорошо. Но реальные проблемы поджидают при инициализации данного богатства. Поставить OpenMP 2.0 на устрой. Чтобы к этому добавить еще сетевой стек у меня ушло более двух недель. При этом стало намного удобнее работать в makefile, чем пытаться давить вредную и глючную CCSv6. Fftlib тоже то еще приключение. Она несет тонны зависимостей на framework components, но все модули из набора fc не содержат нужных функций ибо скомпилированы они без inline функций (директива компилятора без оптимизации). В общем примеры на каждую библиотеку простые и понятные. Но если нужно использовать несколько пакетов — можно вешаться. Инициализация и кормление зависимостей отнимает на порядки больше времени чем должно. Об этом было бы читать интереснее, так как проблема реальна, а в документации лишь мизерные крохи.


    1. serjeant
      30.12.2016 11:30

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

      Полностью с Вами согласен в данном вопросе, отдельные душевные муки и пригорание к стулу вызывает работа с документацие TI. Когда начинаешь читать один документ, потом переходишь по ссылке в другой, потом в третий и так каскадом с зацикливанием. Последние мои мучения были с реализацией обмена данных с FPGA по протоколу SRIO. Документация только в виде справочных таблиц по регистрам, а как и что настраивать, в какой последовательности и т.д. вообще ничего нет, пришлось решать проблему долгими и мучительными ковыряниями в исходниках примеров и методом научного тыка.
      Не знаю как вы, но я очень тоскую по старому CCS 3, стабильный, работающий и приятный в использовании продукт, а вот эта поделка СCS 4-7 на базе Eclipce вызывает нервное подергивание глаза.


      1. telhin
        30.12.2016 13:32

        Не знаю как вы, но я очень тоскую по старому CCS 3

        Я начал работать на 5 версии, не могу сравнивать.

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

        Работа напрямую с компилятором и линковщиком через makefile позволяет избегать большей части eclipse кошмара. Однако в отладке таких ошибок не избежать. ЕМНИП c 12 года ошибка при просмотре ресурсов платы (ROV), если не дай бог хоть одна секция памяти оказалась дальше 0x80000000. А это собственно начало DDR3 памяти и тот же OpenMP требует себе стека в DDR3.

        Есть пакеты которые в принципе не работают, например ndk_2_21_01_38, но они все также доступны на сайте TI.

        В общем контроллер вроде как хороший, RTOS sys/bios сносный, IDE отстой, документация отстой. Под ардуину в блокноте без intelsence писать и то приятнее.


        1. serjeant
          01.01.2017 20:06

          Кстати тоже столкнулся с неработающим ndk, пробовал разные версии, но так и не запустил.

          Работа напрямую с компилятором и линковщиком через makefile позволяет избегать большей части eclipse кошмара.
          Можно подробней о таком методе разработки?


          1. telhin
            03.01.2017 14:08

            Все выглядит достаточно просто если вы занимались компилированием через makefile (у меня опыт небольшой). В первую очередь ссылка на немного документации. Примеры того как должна выглядеть компиляция вне CCS можно посмотреть здесь. Собственно отталкиваясь от одного из таких примеров я колхозил свой проект.

            Документация на компилятор cl6x в данном руководстве пользователя, а также в сопутствующих документах.

            Процесс построения приложения выглядит следующим образом:

            • Специальный инструмент xdc парсит *.cfg файл и тянет зависимости которые нужны.
            • Компилятор компилирует и запускает линковщик.
            • Линковщик подтягивает из библиотек/исходников участки кода (как минимум одна библиотека используется всегда libc.a).
            • PROFIT

            Выгода в данном подходе состоит в том, что можно работать полностью во внешнем редакторе (ужасно бесит как CCS пытается сверстать (секунд 5-6) вебстраничку для *.cfg, но не справляется).
            В случае подтягивания библиотек вся работа происходит в достаточно неудобном многоуровневом графическом интерфейсе (медленно).
            CCS долгое переключение модулей RTOS + тупая ошибка, когда CCS не видит переименованный каталог среди модулей.

            Насчет NDK: для него необходимо проинициализировать ряд других подсистем. Среди них есть QMSS, которая также используется в OpenMP, SRIO(?) и пути инициализации у них разные. Можно посмотреть мой рабочий черновик; вероятно данный пример не получится скомпилировать ибо приходилось перекомпилировать исходники + FFTlib не работает, но как пример с удолетворенными зависимостями посмотреть можно.


            1. serjeant
              03.01.2017 14:49

              Благодарю, буду иметь ввиду ваш метод )))


      1. R6MF49T2
        31.12.2016 01:16

        Было бы интересно почитать про обмен с плис. Каких скоростей удалось достич? Почему srio а не e-pci?


        1. serjeant
          01.01.2017 20:02

          У нас есть плата своей разработки, где FPGA Kintex 7 подключена к dsp tms320c6678 по интерфейсу SRIO.
          Мы использовали передачу данных из ПЛИС в DDR память DSP на скорости 5 Gbps с конфигурацией линий srio «one 4x port»


  1. IliaSafonov
    30.12.2016 13:14

    Для начинающих работу с OpenMP написано хорошо и доступно. Тем не менее, есть несколько замечаний и пожеланий.
    1. Поскольку речь идет о программе повышения квалификации, то участвовать в ней могут люди с разным background. Например, я довольно много использовал OpenMP для задач обработки изображений на PC и немного программировал одноядерные DSP. У меня возникло довольно много вопросов.
    a) Мне было бы интересно понять особенности OpenMP на TMS320C66x по сравнению с реализации OpenMP в компиляторах Intel и Microsoft для PC. Есть ли отличия и в чем?
    b) Сила DSP в SIMD инструкциях. Видимо в статье предполагается, что SIMD-векторизация появляется в результате работы компилятора. Однако бывают задачи, когда авто-векторизация компилятором работает недостаточно хорошо и приходится писать intrinsics и/или встроенный ассемблер. Как такой подход сочетается с OpenMP, нет ли особенностей и ограничений?
    c) При использовании С++ нет необходимости явно указывать private для переменных объявленных внутри параллельной секции. Все остальные переменные автоматически shared, если не указано иное в #pragma. Для TMS320C66x также?
    2. Ошибка с sum, которую Вы приводите в статье, является типичным примером ошибки типа «гонки данных» (race conditions). Новички часто делают подобные ошибки, а обнаружить их бывает не просто. Хорошо бы заострить на этом внимание. Когда я показывал студентам такого рода ошибки в их коде, у большинства тутже возникал соблазн все переменные объявлять shared. Почему так не стоит делать лучше показать на замерах времени работы. Вообще, мне кажется, что графики со временем работы для разных примеров сделали бы статью гораздо лучше.
    3. В тексте говорится про «версии OpenMP разработанные TI». Не понял, как это соотносится с версиями стандарта OpenMP? Кстати, если я ничего не путаю, сейчас есть 4-я версия стандарта.
    4. Совершенно правильно отмечено, что не стоит распаралеливать «короткие» циклы, так как есть накладные расходы на создание потоков. Где и как искать оптимум? Обещана отдельная статья. OK. Буду ждать.
    5. Если честно, то я не увидел ориентации примеров на задачи ЦОС. Размер кода линейного КИХ фильтра (свертка) не сильно отличается от скалярного произведения. Вообще, такой процессор очень хорош для обработки изображений и видео.


    1. vsv630
      30.12.2016 15:09

      Версия стандарта OpenMP — это версия набора имеющихся API и их общая идеология. Как эти API и идеологию реализовать на конкретной аппаратной платформе — проблема разработчиков платформы, в данном случае TI. Поэтому есть версия спецификации OpenMP и версия реализации спецификации. На данный момент TI выпустила версию 2.2 спецификации OMP 3.0.

      По поводу графиков времени — спасибо.

      Директивы OpenMP желательно использовать, скажем так, на уровень выше по отношению к оптимизируемым структурам. Тогда компилятор формирует эффективный код внутри параллельного региона, а OpenMP добавляет снаружи свои издержки на распараллеливание. Но возможны и сложности. Например, столкнулись с тем, что директива atomic не дает выигрыша по сравнению c critical как раз по этой причине: директива OpenMP внутри цикла делает невозможной оптимизацию этого цикла.

      Примеры, ориентированы на ЦОС. Здесь подразумевается сравнение с теми примерами, которые содержатся в документации на OpenMP. Там примеры часто замысловатые и неудобные.

      Еще хочу отметить, что курсы повышения квалификации предназначены для программистов DSP, которые хотят познакомиться с новыми инструментальными средствами. То есть основной упор на DSP, а не на OpenMP.

      За комментарии — спасибо!


  1. denis_obrezkov
    30.12.2016 15:11

    У меня есть простой вопрос: а где купить подобные процессоры в России? Или, к примеру, платы Beagleboard-X15.
    Интересно было бы почитать про обработку данных на гетерогенных процессорах: ARM+DSP+GPU, случайно не знаете литературы по этой теме?


    1. serjeant
      03.01.2017 13:50

      Для отладки мы покупали вот такие платы.
      http://www2.advantech.com/Support/TI-EVM/6678le_of.aspx


      1. denis_obrezkov
        04.01.2017 13:35

        Так ведь вопроса «что купить?» и не стоит. Есть вопрос «где купить в России?».


        1. serjeant
          04.01.2017 20:17

          Да, извиняюсь.
          https://www.einfo.ru/store/TMS320C6678/
          http://ru.farnell.com/texas-instruments/tmdsevm6678l/tms320c6678-enet-uart-eval-module/dp/2113704
          http://allchip.ru/2715447/TMDXEVM6678L.html
          Покупка это отдельная песня… смотря в какой конторе вы работаете и есть ли необходимость покупать через реестр добросовестных поставщиков


          1. denis_obrezkov
            05.01.2017 13:45

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


  1. serjeant
    04.01.2017 20:16

    Да, извиняюсь.
    https://www.einfo.ru/store/TMS320C6678/
    http://ru.farnell.com/texas-instruments/tmdsevm6678l/tms320c6678-enet-uart-eval-module/dp/2113704
    http://allchip.ru/2715447/TMDXEVM6678L.html
    Покупка это отдельная песня… смотря в какой конторе вы работаете и есть ли необходимость покупать через реестр добросовестных поставщиков