Общие сведения о C++ AMP
C++ Accelerated Massive Parallelism (C++) ускоряет выполнения кода C++, посредством использования преимуществ параллельной обработки данных на устройствах, таких как модуль графических вычислений (GPU) на дискретной видеокарте.С помощью C++ AMP возможно реализовать алгоритмы обработки многомерных данных таким образом, что их выполнение может быть ускорено с помощью параллелизма на различном оборудовании.Модель программирования C++ AMP включает в себя многомерные массивы, индексацию, передачу памяти, заполнение и библиотеку математических функций.Можно использовать расширения языка C++ AMP для мониторинга, как данные перемещаются из ЦП в графический процессор (GPU) и обратно, что дает возможность улучшить производительность.
Требования к системе
Windows 7, Windows 8, Windows Server 2008 R2 или Windows Server 2012
Уровень 11,0 функции DirectX 11 или последующего оборудования
Для отладки на программном эмуляторе, необходимы Windows 8 или Windows Server 2012.Для отладки на оборудовании, необходимо установить драйверы для видеокарты.Для получения дополнительной информации см. Отладка кода GPU.
Введение
Следующие два примера демонстрируют основные компоненты C++ AMP.Предположим, что требуется добавить соответствующие элементы в два одномерных массива.Например, требуется добавить {1, 2, 3, 4, 5} и {6, 7, 8, 9, 10} для получения {7, 9, 11, 13, 15}.Без использования C++ AMP, можно написать следующий код для добавления чисел и отображения результатов.
#include <iostream>
void StandardMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5];
for (int idx = 0; idx < 5; idx++)
{
sumCPP[idx] = aCPP[idx] + bCPP[idx];
}
for (int idx = 0; idx < 5; idx++)
{
std::cout << sumCPP[idx] << "\n";
}
}
Важные части кода приведены далее:
Данные: данные состоят из трех массивов.Все они имеют один и тот же ранг (один) и длину (пять).
Итерация: Первый цикл for предоставляет механизм для перебора элементов в массивах.Код, который должен выполняться для вычисления суммы содержится в первом блоке for.
Индекс: Переменная idx обращается к отдельным элементам массивов.
Используя C++ AMP, можно написать следующий код.
#include <amp.h>
#include <iostream>
using namespace concurrency;
const int size = 5;
void CppAmpMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[size];
// Create C++ AMP objects.
array_view<const int, 1> a(size, aCPP);
array_view<const int, 1> b(size, bCPP);
array_view<int, 1> sum(size, sumCPP);
sum.discard_data();
parallel_for_each(
// Define the compute domain, which is the set of threads that are created.
sum.extent,
// Define the code to run on each thread on the accelerator.
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);
// Print the results. The expected output is "7, 9, 11, 13, 15".
for (int i = 0; i < size; i++) {
std::cout << sum[i] << "\n";
}
}
Присутствуют те же самые основные элементы, но используются конструкции C++ AMP:
Данные: Используйте массивы C++ для построения трех объектов C++ AMP array_view.Необходимо указать четыре значения для создания объекта array_view: значения данных, ранг, тип элементов и длина объекта array_view в каждом измерении.Ранг и тип передаются как параметры типа.Данные и длина передаются в качестве параметров конструктора.В этом примере массив C++, передаваемый в конструктор, одномерный.Ранг и длина используются для построения прямоугольной формы данных в объекте array_view, а значения данных используются для заполнения массива.Библиотека среды выполнения включает также Класс array, который содержит интерфейс, похожий на класс array_view и рассматривается далее в этой статье.
Итерация: Функция parallel_for_each (C++ AMP) предоставляет механизм для перебора элементов данных или вычислительного домена.В этом примере, вычислительный домен задан при помощи sum.extent.Код, который необходимо выполнить, содержится в лямбда-выражении или функции ядра.restrict(amp) указывает, что используется только то подмножество языка C++, выполнение которого может ускорить C++ AMP.
Индекс: переменная Класс index, idx, объявляется вместе с рангом, соответствующим рангу объекта array_view.С помощью индекса можно получить доступ к отдельным элементам объектов array_view.
Формирование и индексирование данных: индекс и границы
Необходимо задать значения данных и объявить форму данных перед выполнением кода ядра.Все данные определены как массив (прямоугольный), и можно задать массив любого ранга (число измерений).Данные могут быть любого размера в любом измерении.
Класс index
Класс index указывает местоположение в объекте array или array_view, инкапсулируя смещение от начальной позиции в каждом измерении в один объект.При обращении к расположению в массиве, передается объект index оператору индексирования [] вместо списка целочисленных индексов.Можно получить доступ к элементам в каждом измерении с помощью Оператор array::operator() или Оператор array_view::operator().
Следующий пример создает одномерный индекс, который задает третий элемент одномерного массива объекта array_view.Индекс используется для печати третьего элемента в объекте array_view.Результат равен 3 .
int aCPP[] = {1, 2, 3, 4, 5};
array_view<int, 1> a(5, aCPP);
index<1> idx(2);
std::cout << a[idx] << "\n";
// Output: 3
В следующем примере создается двумерный индекс, который задает элемент, расположенный в первой строке и во втором столбце в объекте array_view.Первый параметр в конструкторе index — строка, а второй параметр — столбец.Результат равен 6 .
int aCPP[] = {1, 2, 3,
4, 5, 6};
array_view<int, 2> a(2, 3, aCPP);
index<2> idx(1, 2);
std::cout << a[idx] << "\n";
// Output: 6
В следующем примере создается трехмерный индекс, который задает элемент, расположенный на глубине = 0, в строке = 1, и столбце = 3 в трехмерном объекте array_view.Обратите внимание, что первый параметр — глубина, второй параметр — строка, а третий параметр — столбец.Результат равен 8 .
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
array_view<int, 3> a(2, 3, 4, aCPP);
// Specifies the element at 3, 1, 0.
index<3> idx(0, 1, 3);
std::cout << a[idx] << "\n";
// Output: 8
Класс extent
Касс extent (C++ AMP) определяет длину данных в каждом измерении объекта array или array_view.Можно создать объект границы и использовать его для создания объекта array или array_view.Можно также получить границы из существующего объекта array или array_view.В следующем примере выводится длина границы в каждом измерении объекта array_view.
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// There are 3 rows and 4 columns, and the depth is two.
array_view<int, 3> a(2, 3, 4, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0]<< "\n";
std::cout << "Length in most significant dimension is " << a.extent[0] << "\n";
В следующем примере создается объект array_view, который имеет те же измерения как и объект в предыдущем примере, но в этом примере используется объект extent вместо использования явных параметров в конструкторе array_view.
int aCPP[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
extent<3> e(2, 3, 4);
array_view<int, 3> a(e, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0] << "\n";
Перемещение данных на ускоритель: array и array_view
Два контейнера данных, используемых для перемещения данных на ускоритель, определены в библиотеке времени выполнения.Это Класс array и Класс array_view.Класс array — это класс контейнера, который создает глубокую копию данных, при создании объекта.Класс array_view — класс-оболочка, который копирует данные, когда функция ядра осуществляет доступ к данным.Когда данные необходимы на исходном устройстве, данные копируются назад.
Класс array
При создании объекта array на ускорителе создается глубокая копия данных, если был использован конструктор, принимающий указатель на набор данных.Функция ядра изменяет копию на ускорителе.Когда выполнение функции ядра завершено, следует скопировать данные обратно в исходную структуру данных.В следующем примере каждый элемент вектора умножается на 10.После завершения функции ядра используется векторный оператор преобразования для копирования данных обратно в объект вектора.
std::vector<int> data(5);
for (int count = 0; count < 5; count++)
{
data[count] = count;
}
array<int, 1> a(5, data.begin(), data.end());
parallel_for_each(
a.extent,
[=, &a](index<1> idx) restrict(amp)
{
a[idx] = a[idx] * 10;
}
);
data = a;
for (int i = 0; i < 5; i++)
{
std::cout << data[i] << "\n";
}
Класс array_view
array_view обладает практически теми же членами, что класс array, но другой основной функциональностью.Данные, передаваемые конструктору array_view, не копируются на графический процессор (GPU), как в случае с конструктором array.Вместо этого данные копируются в ускоритель при выполнении функции ядра.Таким образом, при создании двух объектов, array_view, использующих одни и те же данные, оба объекта array_view ссылаются на одну и ту же область памяти.При этом необходимо синхронизировать любой многопоточный доступ.Главное преимущество использования класса array_view заключается в том, что данные перемещаются только при необходимости.
Сравнение классов array и array_view
В следующей таблице перечислены сходства и различия между классами array и array_view.
Описание |
array - класс |
array_view - класс |
---|---|---|
Определение ранга |
Во время компиляции. |
Во время компиляции. |
Определение границ |
Во время выполнения. |
Во время выполнения. |
Фигура |
Прямоугольная. |
Прямоугольная. |
Хранение данных |
Является контейнером данных. |
Является оболочкой данных. |
Копировать |
Явное глубокое копирование во время определения. |
Неявное копирование при обращении функцией ядра. |
Извлечение данных |
Путем копирования данных массива обратно в объект потока на ЦП. |
Прямым доступом объекта array_view или путем вызова Метод array_view::synchronize, чтобы продолжить доступ к данным в исходном контейнере. |
Выполнение кода над данными: parallel_for_each
Функция parallel_for_each определяет код, который должен быть выполнен на ускорителе в отношении данных в объекте array или array_view.Рассмотрим следующий код, приведенный в начале этого раздела.
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddArrays() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
Метод parallel_for_each принимает два аргумента, вычислительный домен и лямбда-выражение.
Вычислительный домен — это объект extent или tiled_extent, определяющий набор потоков, используемых для параллельного выполнения.Один поток создается для каждого элемента вычислительного домена.В этом случае, объект extent является одномерным и имеет пять элементов.Поэтому будет запущено пять потоков.
Лямбда-выражение задает код для выполнения в каждом потоке.Предложение отслеживания, [=], указывает, что тело лямбда-выражения обращается ко всем собранным переменным по значению, которые в данном случае являются a, b и sum.В этом примере список параметров формирует одномерную переменную типа index с именем idx.Значение idx[0] — 0 в первом потоке и последовательно увеличивается с каждым следующим потоком.restrict(amp) указывает, что используется только то подмножество языка C++, выполнение которого может ускорить C++ AMP.Ограничения функций, которые имеют модификатор ограничения, описаны в Предложение ограничения (C++).Дополнительные сведения см. в разделе Синтаксис лямбда-выражения.
Лямбда-выражение может включать в себя код, который будет выполнен, либо оно может вызывать отдельные функции ядра.Функция ядра должна включать модификатор restrict(amp).Следующий пример эквивалентен предыдущему примеру, но в нем вызывается отдельная функция ядра.
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddElements(index<1> idx, array_view<int, 1> sum, array_view<int, 1> a, array_view<int, 1> b) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
void AddArraysWithFunction() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp)
{
AddElements(idx, sum, a, b);
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
Ускорение кода: дробление и барьеры
Можно получить дополнительное ускорение при помощи дробления.При дроблении потоки делятся на равные прямоугольные подмножества или мозаики.Пользователь определяет подходящий размер мозаики на основе набора данных и применяемого алгоритма.Для каждого потока существует доступ к глобальному расположению элемента данных по отношению ко всему array или array_view, а доступ к локальному расположению осуществляется относительно мозаики.Использование локального значения индекса упрощает код, так как не требуется писать код для преобразования глобального значения индекса в локальное.Для использования мозаик необходимо вызвать Метод extent::tile в этом вычислительном домене в методе parallel_for_each и использовать объект tiled_index в лямбда-выражении.
В типичных приложениях элементы мозаики связаны каким-либо образом и код должен осуществлять доступ и отслеживать значения по всей мозаике.Для этого следует использовать ключевое слово tile_static - ключевое слово и Метод tile_barrier::wait.Переменная, которая имеет ключевое слово tile_static имеет область действия по всей мозаике и экземпляр переменной создается для каждой мозаики.Необходимо обрабатывать синхронизацию доступа потока мозаики к переменной.Метод tile_barrier::wait останавливает выполнение текущего потока до тех пор, пока все потоки мозаики не достигнут вызова tile_barrier::wait.Таким образом можно накапливать значения по всей мозаике с помощью переменных tile_static.Затем можно завершить все вычисления, требующие доступа ко всем значениям.
Следующая схема представляет двухмерный массив данных выборки, осуществленной в мозаиках.
Следующий пример кода использует данные выборки из предыдущей схемы.Код заменяет каждое значение в мозаике средним значением всех значений мозаики.
// Sample data:
int sampledata[] = {
2, 2, 9, 7, 1, 4,
4, 4, 8, 8, 3, 4,
1, 5, 1, 2, 5, 2,
6, 8, 3, 2, 7, 2};
// The tiles:
// 2 2 9 7 1 4
// 4 4 8 8 3 4
//
// 1 5 1 2 5 2
// 6 8 3 2 7 2
// Averages:
int averagedata[] = {
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
};
array_view<int, 2> sample(4, 6, sampledata);
array_view<int, 2> average(4, 6, averagedata);
parallel_for_each(
// Create threads for sample.extent and divide the extent into 2 x 2 tiles.
sample.extent.tile<2,2>(),
[=](tiled_index<2,2> idx) restrict(amp)
{
// Create a 2 x 2 array to hold the values in this tile.
tile_static int nums[2][2];
// Copy the values for the tile into the 2 x 2 array.
nums[idx.local[1]][idx.local[0]] = sample[idx.global];
// When all the threads have executed and the 2 x 2 array is complete, find the average.
idx.barrier.wait();
int sum = nums[0][0] + nums[0][1] + nums[1][0] + nums[1][1];
// Copy the average into the array_view.
average[idx.global] = sum / 4;
}
);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 6; j++) {
std::cout << average(i,j) << " ";
}
std::cout << "\n";
}
// Output:
// 3 3 8 8 3 3
// 3 3 8 8 3 3
// 5 5 2 2 4 4
// 5 5 2 2 4 4
Математические библиотеки
С++ AMP включает в себя две математические библиотеки.Библиотека двойной точности в Пространство имен Concurrency::precise_math обеспечивает поддержку функций двойной точности.Она также предоставляет поддержку функций одиночной точности, хотя по-прежнему будет требоваться оборудование с поддержкой двойной точности.Она соответствует спецификации C99 (ISO/IEC 9899).Ускоритель должен полностью поддерживать двойную точность.Это можно определить путем проверки значения Элемент данных accelerator::supports_double_precision.Быстрая математическая библиотека в Пространство имен Concurrency::fast_math содержит другой набор математических функций.Эти функции, поддерживающие только операнды float, выполняются быстрее, но не с такой точностью, как функции в математической библиотеке двойной точности.Функции содержатся в файле заголовка <amp_math.h> и объявлены как restrict(amp).Функции в файле заголовка <cmath> импортированы в пространства имен fast_math и precise_math.Ключевое слово restrict используется, чтобы различать <cmath> версию и C++ AMP версию. Следующий пример кода вычисляет десятичный логарифм каждого значения этого вычислительного домена, используя быстрый метод.
#include <amp.h>
#include <amp_math.h>
#include <iostream>
using namespace concurrency;
void MathExample() {
double numbers[] = { 1.0, 10.0, 60.0, 100.0, 600.0, 1000.0 };
array_view<double, 1> logs(6, numbers);
parallel_for_each(
logs.extent,
[=] (index<1> idx) restrict(amp) {
logs[idx] = concurrency::fast_math::log10(logs[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
Графическая библиотека
C++ AMP включает в себя графическую библиотеку, предназначенную для программирования ускоренной графики.Эта библиотека используется только на устройствах, которые поддерживают базовую графическую функциональность .Методы находятся в Пространство имен Concurrency::graphics и содержатся в файле заголовка <amp_graphics.h>.Ключевые компоненты графической библиотеки:
Класс texture: класс текстуры можно использовать для создания текстуры из памяти или из файла.Текстуры похожи на массивы, поскольку они содержат данные, и они напоминают контейнеры в стандартной библиотеки шаблонов (STL) по части присваивания и копирования.Для получения дополнительной информации см. Контейнеров STL.Параметры шаблона для класса texture: тип элемента и ранг.Ранг может быть 1, 2 или 3.Тип элемента может быть одним из коротких векторных типов, описанных далее в этой статье.
Класс writeonly_texture_view: предоставляет доступ к любой текстуре только для записи.
Short библиотека вектора: определяет набор коротких векторных типов длиной 2, 3 и 4, которые основаны на int, uint, float, double, norm или unorm.
приложения Магазина Windows
Как и другие библиотеки C++, библиотеку C++ AMP можно использовать в приложениях Магазина Windows.Эти статьи описывают, как включить код C++ AMP в приложения, созданные с помощью C++, C#, Visual Basic или JavaScript:
Оптимизатор отключения карт Bing, приложение хранения окна в JavaScript C и C-++
Использование C++ AMP из C#, используя среду выполнения Windows
C++ AMP и визуализатор параллелизма
Визуализатор параллелизма включает в себя поддержку анализа производительности кода C++ AMP.В следующих статьях описаны эти функции:
Рекомендации по производительности
Получение модуля и деление целых чисел без знака, может иметь значительно лучшую производительность, чем получение модуля и деление целых чисел со знаком.Рекомендуется использовать целые числа без знака.