Ядерное партнёрство: Часть 1. Создание высокоэффективных приложений для многоядерных процессоров

RapidMind платформа разработки для приложений Cell/B.E., построенная на принципе единого исходного кода

Платформа разработки RapidMind предоставляет простую технологию, основанную на принципе единого исходного кода (single-source) для разработки высокоэффективных переносимых приложений для многоядерных процессоров. В частности, с её помощью можно разрабатывать приложения, полностью использующие уникальную архитектуру процессора Cell Broadband Engine™ (Cell/B.E.), путём создания всего одной однопоточной программы на C++ с использованием компилятора C++. В этой статье учёный-исследователь Майкл Маккул проводит обзорный тур по платформе RapidMind.

Майкл Маккул, соучредитель и руководитель исследовательских работ, RapidMind

Photo of Michael McCoolДоцент Университета Ватерлоо (University of Waterloo) и соучредитель RapidMind, д-р Маккул совмещает исследования в университете с участием в совете директоров RapidMind Inc. В его исследовательские интересы входят высококачественный рендеринг в реальном времени, глобальное и локальное освещение, аппаратные алгоритмы, параллельные и перестраиваемые вычисления, приложения с использованием методов интервалов и Монте Карло, программирование и метапрограммирование для конечного пользователя, обработка и оцифровка изображений и сигналов.



11.09.2009

Прежде чем приступать к рассмотрению платформы RapidMind, с помощью которой можно разрабатывать приложения для процессора Cell/B.E., эффективно использующие его архитектуру, путём написания единственной однопоточной программы на C++ с помощью существующего компилятора C++, позвольте мне объяснить, почему сочетание процессоров Cell/B.E. и платформы RapidMind даёт хорошую среду разработки приложений.

Введение в RapidMind и Cell/B.E.

Девять ядер процессора Cell/B.E. состоят из:

  • одного "ведущего" ядра общего назначения, PPU, с архитектурой PowerPC®.
  • восьми синергических процессорных элементов (Synergistic Processing Elements, SPE), представляющих собой векторные сопроцессоры, максимально оптимизированные для численных расчётов.

PPU — это процессор общего назначения, на базе которого может работать традиционная операционная система и прикладные программы. Однако большая часть вычислительной мощности процессора Cell/B.E. находится в более специализированных SPE. Каждому ядру SPE присущи уникальные функции, в том числе большой векторный регистровый файл (vector register file, VRF), векторное арифметико-логическое устройство (ALU) и явно управляемая высокоскоростная локальная память. Процессор Cell/B.E. имеет ряд средств, которые позволяют PPU и SPE обмениваться информацией и синхронизироваться друг с другом.

Почему именно Cell/B.E.?

При использовании платформы RapidMind с процессором Cell/B.E. нет необходимости вникать в детали работы ядер SPE или же заниматься SPE-специфичным программированием. Достаточно написать обычную однопоточную программу, используя существующий компилятор C++ (например, g++) и запустить ее на PPU. Программа должна включать единственный заголовочный файл и ссылку на библиотеку платформы RapidMind.

Вычисление, выполняемое на SPE, должно быть описано как вычисление с параллелизмом по данным и должно выполняться с использованием платформы RapidMind. Интерфейс платформы RapidMind реализован в виде библиотеки, которая прозрачно интегрируется с существующими IDE и средами сборки. Несмотря на библиотечную реализацию, платформу RapidMind можно вполне представить себе в виде высокоэффективного встроенного языка параллельного программирования с собственным управлением кодом и средой параллельного исполнения. С помощью интерфейса RapidMind можно описывать любые вычисления и выполнять их с уровнями производительности, конкурирующими с производительностью программ, написанных в нативных, аппаратно-зависимых инструментальных средах. При этом RapidMind предоставляет простую, удобную в сопровождении и переносимую модель программирования.

Интерфейс RapidMind основан на небольшом наборе типов, имитирующих стандартные типы языка C++: числа с плавающей запятой, массивы и функции; это предельно упрощает перенос существующего кода. Однако вычисления, выраженные типами RapidMind, могут быть объединены в вычислительно-интенсивные ядра, динамически компилируемые непосредственно на оптимизированный машинный язык SPE, и работать параллельно под управлением сложной исполняющей системы с автоматическим распределением нагрузки и синхронизацией с главным ядром. Платформа RapidMind автоматизирует большинство задач нижнего уровня, связанных с использованием дополнительных высокоэффективных ядер SPE, облегчая реализацию преимуществ колоссальной вычислительной производительности, предоставляемой процессором Cell/B.E. для широкого диапазона приложений.

Создаём высокопроизводительные программы на C++

Язык программирования C++ обычно считается неподходящим для высокопроизводительного программирования. Модульные механизмы и модель памяти языка C++, к сожалению, влекут за собой системные издержки, которые трудно устранить традиционными стратегиями компиляции. Однако архитектура платформы RapidMind, используя собственную стратегию динамической компиляции, обходит эти проблемы, устраняя издержки и позволяя создавать программы для таких нетрадиционных архитектур, как SPE.

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

Далее в статье будет дано краткое введение в платформу RapidMind, описаны основные типы и модель программирования, и на примере будет продемонстрировано, как простое ядро цикла преобразуется для работы на SPE.

Интерфейс и модель программирования RapidMind

Интерфейс RapidMind основан на трех основных типах:

  • тип Value,
  • тип Array и
  • тип Program.

Эти типы определяются во включаемом файле rapidmind/platform.hpp. Включение этого файла и ссылка на библиотеку rmplatform — это всё, что необходимо, чтобы начать использовать платформу. Пространство имен rapidmind защищает объявления типов RapidMind от конфликта имен с типами, определяемыми пользователем. Кроме того, для выражения управления потоками используется некоторое количество макросов, обычно защищённых префиксом "RM_". Включением rapidmind/shortcuts.hpp можно определить некоторые псевдонимы, опускающие эти префиксы. Для краткости в своих примерах я буду использовать такие псевдонимы, а также опущу определения пространства имен.

Тип Value

Тип Value<N,T>— это контейнер фиксированного размера, однородный кортеж с N экземплярами типа T. Тип элемента T может быть любым основным числовым типом C++, например float или int. Тип T может также быть bool.

Имеются также короткие формы записи для задания значений кортежей с числом элементов до четырёх включительно. Например, Value3f обозначает кортеж с тремя числами с плавающей запятой одинарной точности, а Value4ui означает кортеж из четырёх беззнаковых целых. Тип Value<1,T> в большинстве случаев напрямую заменяется единичным экземпляром типа T. Например, комплексные числа с одинарной точностью на платформе RapidMind могут быть реализованы при помощи std::complex<Value1f>.

Все обычные операторы перегружаются значениями; они действуют покомпонентно. Большинство функций стандартной библиотеки C также распространяются на заданные аргументы и выполняются параллельно на каждом элементе. Типы value также предоставляют поддержку перестановки (swizzling) и маску записи (writemasking):

  • Перестановка (swizzling) предоставляет возможность произвольной перестановки компонентов значения кортежа.
  • Маска записи (writemasking) позволяет разработчику записывать только в подмножество компонентов значения кортежа в адресате присваивания.

Например, имея 4-элементный кортеж Value4f c, можно извлечь первые три компонента c и изменить их порядок на обратный с помощью выражения перестановки c(2,1,0). Эта перестановка за одну операцию извлекает компоненты 2, 1, и 0 и упаковывает их в 3-элементный кортеж. Тип value обеспечивает явное определение векторизованных вычислений, которые хорошо соответствуют векторной регистровой архитектуре SPE.

Тип Array

Тип Array<D,V> представляет многомерный контейнер переменной длины, где D— это размерность (1, 2, или 3), а V— тип элемента (значение RapidMind). Этот тип предназначен для управления большими объёмами данных. Как и обычные массивы указателей, используемые в C++, массивы RapidMind можно присвоить друг другу за время O(1). Однако массивы RapidMind используют семантику по значению (by-value), а не по ссылке (by-reference). Это упрощает управление памятью и помогает избежать ненужных побочных эффектов.

Уникальный тип Program

Наконец, RapidMind поддерживает уникальный тип Program, представляющий вычисление. В сущности, это контейнер для кода программы, конструируемый динамически. Его можно представить как функцию, которая, в отличие от обычных функций C++, создаётся и управляется во время исполнения программы. Фактически последовательности вычислений над значениями RapidMind могут быть "записаны" и сохранены в программном объекте RapidMind. Вот простой пример кода, показывающий, как может быть создан программный объект:

Program prog = BEGIN {
  In<Value3f> a, b;
  Out<Value3f> c;

  Value3f d = func(a, b);
  c = d + a * 2.0f;
} END;

В этом примере вызывается C++ функция func(). Эта функция может быть определена обычными механизмами C++. Также могут использоваться многие другие модульные конструкции C++, например классы. Значение d – это локальная переменная, невидимая вне программы. Значения a и b помечены как входные, а значение c— как выходное. Входы инициализируются значениями фактических аргументов, а значения, помеченные как выход, позже, во время сборки и выполнения программы, будут скопированы в фактический выход. Программы могут иметь больше одного входа и больше одного выхода.

Макрос BEGIN переключает из режима "immediate" в режим "retained". По умолчанию операции на типах RapidMind фактически выполняются на главном ядре точно так же, как и числовые типы C++, которые они эмулируют. В режиме "retained", однако, операции не выполняются, они записываются и сохраняются в собранном программном объекте. При наличии тэга END, платформа RapidMind снова переключается в режим "immediate", но также и подготавливает захваченные операции, сохраненные в программном объекте, для выполнения на SPE.

Заметим, что операции на типах RapidMind захватываются только тогда, когда программный объект уже построен. Это тот механизм, который позволяет платформе избегать издержек, присущих C++. Фактически обычные модульные конструкции C++ действуют как каркас для генерации последовательностей интенсивных вычислений, но непроизводительные нагрузки на систему для этих модульных конструкций возникают только один раз, во время создания программы, а не каждый раз, когда она выполняется.

По сути конструкции C++ "впаиваются" в объект программы — указатели разыменовываются, тела функций встраиваются, циклы развертываются, переменные C++ интерпретируются как константы, что в результате даёт очень плотный и эффективный код. Платформа RapidMind, однако, имеет динамические версии всех этих конструкций, которые можно использовать тогда (и только тогда), когда в них есть необходимость.

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

Синтаксически конструкция программного объекта выглядит как динамическое определение функции, а получающийся в итоге объект можно использовать практически как обычную функцию. Однако программные объекты RapidMind, как правило, применяются ко всем массивам в целом. Вычисление, выраженное в программе, выполняется параллельно на всех элементах массива, с одновременным использованием всех ядер процессора Cell/B.E. Если A, B, и C являются объектами массива RapidMind, то параллельное исполнение инициализируется следующим образом: C = prog(A,B);.

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

Program p = BEGIN {
  In<Value3f> a;
  In<Value1i> u;
  Out<Value3f> c;
  
  Value3f d = func(a, B[u]);
  IF (all(a > 0.0f)) {
    c = d + a * 2.0f;
  } ELSE {
    c = d - a * 2.0f;
  } ENDIF;
} END;

Включение управления потоками делает модель программирования очень общей. Технически RapidMind использует модель программирования SPMD (Single Program Multiple Data) с параллелизмом по данным. Также допускаются некоторые коллективные операции, включая редукции высокого порядка, которые могут принимать в качестве аргумента ассоциативную программу-"объединитель".

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

Модель программирования с параллелизмом по данным, используемая платформой RapidMind, позволяет элегантно масштабировать систему к переменному числу ядер. Например, на Sony® PLAYSTATION® 3 с установленным Yellow Dog Linux (от компании Terra Soft) программа RapidMind может выполняться параллельно на шести доступных ядрах SPE. На blade-сервере IBM QS20 Cell/B.E., который имеет два связанных процессора Cell/B.E., та же самая программа автоматически задействует все 16 доступных ядер SPE.

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

Пример преобразования цикла

Этот дополнительный пример должен прояснить принципы интерфейса RapidMind. Ниже я преобразую вложенное ядро цикла в параллельную реализацию, выполняемую на всех SPE:

#include <cmath>

const int w = 512, h = 512;
float f;
float a[w][h][4], b[w][h][4];

void compute() {
   for (int i = 0; i < w; i++) 
      for (int j = 0; j < h; j++) 
         for (int k = 0; k < 4; k++) {
            a[i][j][k] += f * b[i][j][k];
   }
}

Процесс преобразования содержит три шага, показанных на рисунке 1.

Рисунок 1. Диаграмма преобразования RapidMind
Диаграмма преобразования RapidMind
  • Первый шаг: числовые типы, используемые в ядре, преобразуются в эквивалентные типы RapidMind.
  • Второй шаг: создание программного объекта RapidMind.
  • Третий шаг: программный объект применяется к массиву данных, что вызывает параллельное вычисление, координируемое диспетчером исполнения.

Для использования платформы необходимо включить заголовочные файлы RapidMind. Для краткости в примерах я также загружу краткие формы макросов и определю глобальное использование пространства имен rapidmind:

#include <rapidmind/platform.hpp>
#include <Rapidmind/shortcuts.hpp>
using namespace rapidmind;

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

const int w = 512, h = 512;
Value1f f;
Array<2,Value4f> a(w,h), b(w,h);

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

Program compute_prog;
void init_compute_prog () {
   compute_prog = BEGIN {
      In<Value4f> r, s;
      Out<Value4f> t;
      t = r + f * s;
   } END;
}

Заметьте, что программный объект фактически обращается к нелокальной переменной f типа Value1f. Когда программный объект запустится на ядрах SPE, он будет использовать существующее на момент запуска значение этой переменной, так же, как и обычная функция C++. Платформа RapidMind автоматически отслеживает зависимости между программными объектами и ссылками на нелокальные переменные, объявленные с использованием типов RapidMind, а также настраивает соответствующее соединение без дальнейшего вмешательства разработчика. Длина 1-элементного кортежа f также автоматически распространяется на 4-элементный кортеж, дублированием его скалярной величины перед операцией покомпонентного умножения.

Наконец, программу можно запустить, когда понадобится:

a = compute_prog(a,b);

Этот пример иллюстрирует ещё один дополнительный момент: отметьте, что массив a является и вводом и выводом программы. Платформа RapidMind использует семантику параллельного присваивания — вводы всегда привязаны к "предыдущему", а не к "новому" значению.

Для тех, кто хочет узнать больше

В этой статье даётся краткое введение в платформу RapidMind. За дополнительной информацией, включая результаты тестов производительности и более детальными примерами кода, обращайтесь на официальный сайт RapidMind (см. Ресурсы), где опубликовано много технических обзоров. Также можно подписаться на ознакомительную версию платформы RapidMind, с полной документацией и примерами множества приложений.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



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

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=427738
ArticleTitle=Ядерное партнёрство: Часть 1. Создание высокоэффективных приложений для многоядерных процессоров
publish-date=09112009