Строим приложение из Array Building Blocks

В этом выпуске: присматриваемся к новому продукту Intel® Array Building Blocks (Intel® ArBB), пишем ArBB приложение для подсчета суммы чисел в массиве, а также пытаемся оценить преимущества данного подхода и вызванную им головную боль.

Есть много решений, упрощающих разработку параллельных приложений. Один из первых стандартов в области параллельных технологий – Cilk, разработка которого началась в 1994 году в лаборатории Информатики M.I.T. Кстати, c 2009 года группа разоработчиков Cilk стала частью Intel. В 1997 году появилась первая версия другого решения – OpenMP. Сейчас это одна из наиболее распространенных технологий, ключевыми элементами которой являются конструкции для управления потоками и данными, библиотечные функции и переменные окружения. В 2006 году Intel выпустила Intel® Threading Building Blocks (Intel® TBB), где параллелизм реализован на уровне задач, а в распоряжение разработчиков предоставлены планировщик и потокобезопасные контейнеры. Продукт получился весьма успешным и используется как основа для других решений, в частности, ArBB, на котором остановимся подробнее.

Основная идея – прячем параллельный код по контейнерам.

Intel® ArBB представляет собой расширение C++ новыми классами и функциями, а также собственный runtime. Продукт появился в результате слияния своего предшественника Intel’s Ct Technology c технологией Rapidmind. Cуть ArBB в том, что программист пишет код, похожий на обычный, непараллельный, но использует типы данных, контейнеры и функции из ArBB (например, i32 вместо int, dense вместо vector и т.д.) и указывает, выполение каких функций должен взять на себя ArBB runtime. По умолчанию runtime сам определяет, сколько потоков создать, что каждый из них будет делать, как синхронизовать данные и т.д. Посмотрим как это выглядит на деле. Для этого разберем небольшой пример кода, в котором используется ArBB.

Hello ArBB! Как это выглядит на практике?

Одна из простых типовых задач – подсчет суммы элементов в массиве целых чисел. Код с использованием ArBB выглядит следующим образом.


#include <iostream>

#include <arbb.hpp>

// включение основного include файла ArBB


template <typename T>

void f(arbb::dense<T> arr, T& res)

// arr – исходный контейнер, в res – записывается результат 

{

    res = arbb::add_reduce(arr);

    // использование встроенной ArBB функции

    // для подсчета суммы элементов

}


int main()

{

    int arr_size = 500;    // размер массива

    int* arr = (int*)arbb::aligned_malloc(sizeof(int)*arr_size);

    // выделение памяти под массив

    // использование arbb::aligned_malloc

    // гарантирует корректную работу ArBB с массивом

    for(int i = 0; i<arr_size;i++)  // инициализация массива

        arr[i]=i;

    arbb::dense<arbb::i32> arbb_arr;

    // декларируем контейнер ArBB

    arbb::bind(arbb_arr, arr, arr_size);

    // связываем исходный массив и контейнер ArBB

    arbb::i32 result;

    // объявляем переменную для записи результата

    arbb::call(f<arbb::i32>)(arbb_arr, result);

    // вызов ArBB runtime для выполнения функции f


    std::cout << arbb::value(result);

    // Доступ к ArBB скалярам осуществляется через arbb::value


    return 0;

}



В примере реализована функция f, принимающая два аргумента. Первый – исходный контейнер dense, что в терминах ArBB является аналогом vector. Cумму его элементов мы и будем считать. Второй аргумент нужен, чтобы вернуть результат.

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

В main() создается и инициализируется массив, объявляется ArBB контейнер, и происходит связывание массива и контейнера. Зачем все это? В использовании ArBB есть нюанс: ArBB может производить вычисления только в своем пространстве и только с ArBB типами (нельзя передать C++ массив напрямую в f). Теперь же массив и ArBB контейнер содержат одни и те же данные. Далее объявляем ArBB переменную для записи результата и вызываем функцию f. arbb::call(имя)(аргументы) – это и есть вызов ArBB runtime с указанием выполнить функцию, исполнение происходит в ArBB пространстве. Осталось вывести результат. Единственное, что надо не забыть – это прописать зависимость от библиотеки ArBB в свойствах проекта или напрямую в командной строке при компиляции, например

icl /D WIN32 /D NDEBUG /EHsc application.cpp /link arbb.lib

Что имеем в результате: минусы


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

  • Собственный runtime приносит дополнительные накладные расходы.



Что имеем в результате: плюсы


  • Гарантированное отсутствие data races & deadlocks.

  • Высокоуровневый API: сотни операций с контейнерами, в том числе и многомерными, делаются в одну строчку посредством встроенных функций, не загромождая код и мозг ненужными деталями.

  • Собственный runtime обеспечивает отличную переносимость приложений. Не надо ничего изменять, чтобы приложение оптимально работало и на multi-, и на manycore системах, будь то 4-х ядерный десктоп, 16-core двухсокетный сервер или 32-ядерная MIC карта. Также нет необходимости оптимизировать приложение под конкретный набор SIMD инструкций. Это большое преимущество как с точки зрения цены поддержки приложения, так и в случае если вы не знаете заранее, на каком железе оно будет запускаться.



Лучшее место для начала знакомства с ArBB, пожалуй, здесь. Продукт сейчас в Beta стадии.
Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.