Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
Заголовки C++ AMP устарели начиная с Visual Studio 2022 версии 17.0.
Включение любых заголовков AMP приведет к возникновению ошибок сборки. Определите _SILENCE_AMP_DEPRECATION_WARNINGS перед включением всех заголовков AMP, чтобы подавить предупреждения.
Ускорение массивного параллелизма C++ (C++ AMP) ускоряет выполнение кода C++ за счёт использования аппаратного обеспечения для параллельной обработки данных, такого как графический процессор (GPU) на дискретной графической карте. С помощью C++ AMP можно кодировать многомерные алгоритмы данных, чтобы ускорить выполнение с помощью параллелизма на разнородном оборудовании. Модель программирования C++ AMP включает многомерные массивы, индексирование, передачу памяти, плитку и библиотеку математических функций. Расширения языка C++ AMP можно использовать для управления перемещением данных с ЦП на GPU и обратно, чтобы повысить производительность.
Требования к системе
Windows 7 или более поздней версии.
Windows Server 2008 R2 до Visual Studio 2019.
Оборудование DirectX 11 уровня возможностей 11.0 или более поздней версии
Для отладки в эмуляторе программного обеспечения требуется Windows 8 или Windows Server 2012. Для отладки на оборудовании необходимо установить драйверы для графической карты. Дополнительные сведения см. в разделе Отладка кода GPU.
Примечание. AMP в настоящее время не поддерживается в ARM64.
Введение
В следующих двух примерах показаны основные компоненты 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_viewкласс и рассматривается далее в этой статье.Итерация: функция parallel_for_each (C++ AMP) предоставляет механизм для итерации элементов данных или вычислительного домена. В этом примере вычислительный домен задается с помощью
sum.extent. Код, который требуется выполнить, содержится в лямбда-выражении или функции ядра. Указываетrestrict(amp), что используется только та часть языка C++, которую может ускорить C++ AMP.Индекс: переменная класса индекса ,
idxобъявляется с рангом одного для сопоставления рангаarray_viewобъекта. Используя индекс, вы можете получить доступ к отдельным элементамarray_viewобъектов.
Формирование и индексирование данных: индекс и размер
Перед запуском кода ядра необходимо определить значения данных и объявить форму данных. Все данные определяются как массив (прямоугольный), и можно определить массив для любого ранга (число измерений). Данные могут иметь любой размер в любом из измерений.
Класс 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
В следующем примере создается двухмерный индекс, указывающий элемент, в котором строка = 1 и столбец = 2 в двухмерном 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
Класс экстент
Класс экстентов указывает длину данных в каждом измерении 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_view. Класс array — это класс контейнера, который создает глубокую копию данных при создании объекта. Класс array_view является классом-оболочкой, который копирует данные, когда функция ядра обращается к данным. Когда данные необходимы на исходном устройстве, данные копируются обратно.
Класс array
array При построении объекта на акселераторе создается глубокая копия данных, если используется конструктор, содержащий указатель на набор данных. Функция ядра изменяет копию на ускорителе. После завершения выполнения функции ядра необходимо скопировать данные обратно в структуру исходных данных. В следующем примере каждый элемент в векторе умножается на 10. После завершения функции ядра vector conversion operator используется для копирования данных обратно в объект вектора.
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, но базовое поведение не совпадает. Данные, передаваемые array_view конструктору, не реплицируются на GPU, так как это происходит с конструктором array . Вместо этого данные копируются в акселератор при выполнении функции ядра. Поэтому, если вы создаете два array_view объекта, использующих те же данные, оба array_view объекта ссылаются на одно и то же пространство памяти. При этом необходимо синхронизировать любой многопоточный доступ. Основное преимущество использования array_view класса заключается в том, что данные перемещаются только в том случае, если это необходимо.
Сравнение массива и array_view
В следующей таблице приводится сводка сходств и различий между классами array и array_view.
| Описание | Класс массива | класс array_view |
|---|---|---|
| При определении ранга | Во время компиляции. | Во время компиляции. |
| Когда определяется область | Во время выполнения. | Во время выполнения. |
| Форма | Прямоугольный. | Прямоугольный. |
| Хранение данных | Контейнер данных. | Является оболочкой данных. |
| Копия | Явное и глубокое копирование при определении. | Неявное копирование при доступе к функции ядра. |
| Извлечение данных | Путем копирования данных массива обратно в объект на поток ЦП. | Путем прямого доступа к array_view объекту или путем вызова метода array_view::synchronize для продолжения доступа к данным в исходном контейнере. |
Общая память с массивом и array_view
Общая память — это память, доступ к которой осуществляется как ЦП, так и акселератором. Использование общей памяти устраняет или значительно сокращает затраты на копирование данных между ЦП и акселератором. Хотя память предоставляется совместно, доступ к ней не может одновременно выполняться как ЦП, так и акселератором, и это приводит к неопределенному поведению.
array Объекты можно использовать для указания точного управления использованием общей памяти, если связанный акселератор поддерживает его. Определяется ли ускоритель поддерживает общую память, свойством supports_cpu_shared_memory акселератора, которое возвращает true, когда поддержка общей памяти имеется. Если поддерживается разделяемая память, по умолчанию перечисление access_type для выделения памяти на акселераторе определяется свойством default_cpu_access_type. По умолчанию объекты array и array_view принимают тот же access_type что и основной связанный accelerator.
Явно задав свойство array, вы можете осуществлять точный контроль над использованием общей памяти, чтобы оптимизировать приложение с учётом характеристик производительности оборудования в зависимости от шаблонов доступа к памяти вычислительных ядер. Выражение array_view отражает то же cpu_access_type, что и array, с которым оно связано; или, если array_view создается без источника данных, его access_type отражает среду, которая сначала вызывает выделение хранилища. То есть, если сначала к узлу (ЦП) обращаются, он ведет себя так, как если бы был создан на источнике данных ЦП и разделяет access_type и accelerator_view, связанные с захватом. Однако если сначала к нему обращаются с помощью accelerator_view, то он ведет себя так, как если бы был создан на array, сделанном на этом accelerator_view, и разделяет arrayaccess_type.
В следующем примере кода показано, как определить, поддерживает ли акселератор по умолчанию общую память, а затем создает несколько массивов с разными конфигурациями cpu_access_type.
#include <amp.h>
#include <iostream>
using namespace Concurrency;
int main()
{
accelerator acc = accelerator(accelerator::default_accelerator);
// Early out if the default accelerator doesn't support shared memory.
if (!acc.supports_cpu_shared_memory)
{
std::cout << "The default accelerator does not support shared memory" << std::endl;
return 1;
}
// Override the default CPU access type.
acc.default_cpu_access_type = access_type_read_write
// Create an accelerator_view from the default accelerator. The
// accelerator_view inherits its default_cpu_access_type from acc.
accelerator_view acc_v = acc.default_view;
// Create an extent object to size the arrays.
extent<1> ex(10);
// Input array that can be written on the CPU.
array<int, 1> arr_w(ex, acc_v, access_type_write);
// Output array that can be read on the CPU.
array<int, 1> arr_r(ex, acc_v, access_type_read);
// Read-write array that can be both written to and read from on the CPU.
array<int, 1> arr_rw(ex, acc_v, access_type_read_write);
}
Выполнение кода над данными: 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++ AMP). Дополнительные сведения см. в описании синтаксиса лямбда-выражения.
Лямбда-выражение может включать код для выполнения или вызвать отдельную функцию ядра. Функция ядра должна включать 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
Математические библиотеки
C++ AMP включает две математические библиотеки. Библиотека двойной точности в пространстве имен Concurrency::precise_math поддерживает функции с двойной точностью. Она также обеспечивает поддержку функций с одинарной точностью, хотя поддержка двойной точности со стороны оборудования по-прежнему необходима. Он соответствует спецификации C99 (ISO/IEC 9899). Акселератор должен поддерживать полную двойную точность. Вы можете определить, выполняется ли это, проверяя значение элемента данных акселератора::supports_double_precision. Быстрая математическая библиотека в пространстве имен Concurrency::fast_math содержит другой набор математических функций. Эти функции, поддерживающие только float операнды, выполняются быстрее, но не так точны, как в библиотеке математики двойной точности. Функции содержатся в файле заголовка <amp_math.h> , и все они объявляются с restrict(amp). Функции в <заголовочном файле cmath> импортируются в оба пространства имен: fast_math и precise_math. Ключевое restrict слово используется для различения <версии cmath> и версии C++ AMP. Следующий код вычисляет логарифм base-10, используя быстрый метод каждого значения, которое находится в вычислительном домене.
#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(numbers[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
Библиотека графики
C++ AMP включает в себя графическую библиотеку, предназначенную для ускорения графического программирования. Эта библиотека используется только на устройствах, поддерживающих собственные графические функции. Методы находятся в пространстве имен Concurrency::graphics и содержатся в <заголовочном файле amp_graphics.h>. Ключевыми компонентами графической библиотеки являются:
Класс текстуры. Класс текстуры можно использовать для создания текстур из памяти или из файла. Текстуры похожи на массивы, так как они содержат данные, и они похожи на контейнеры в стандартной библиотеке C++ в отношении назначения и копирования конструкции. Дополнительные сведения см. в разделе "Стандартные контейнеры библиотеки C++". Параметры шаблона для
textureкласса — это тип элемента и ранг. Ранг может быть 1, 2 или 3. Тип элемента может быть одним из типов коротких векторов, описанных далее в этой статье.класс writeonly_texture_view: предоставляет доступ только для записи к любой текстуре.
Библиотека коротких векторов: определяет набор типов коротких векторов длины 2, 3 и 4, основанных на
int,uint,float,double, норме или ненорме.
Приложения универсальной платформы Windows (UWP)
Как и другие библиотеки C++, вы можете использовать C++ AMP в приложениях UWP. В этих статьях описывается, как включить код C++ AMP в приложения, созданные с помощью C++, C#, Visual Basic или JavaScript:
Оптимизатор поездок Bing Maps, приложение Магазина Windows на JavaScript и C++
Использование C++ AMP из C# с помощью среды выполнения Windows
Визуализатор C++ AMP и Визуализатор параллелизма
Визуализатор параллелизма включает поддержку анализа производительности кода C++ AMP. В этих статьях описаны следующие функции:
Рекомендации по производительности
Модулы и деление целых чисел без знака имеют значительно лучшую производительность, чем модулы и деление целочисленных чисел со знаком. Рекомендуется использовать целые числа без знака, если это возможно.
См. также
C++ AMP (C++ Ускоренная массовая параллелизм)
Синтаксис лямбда-выражения
Справочник (C++ AMP)
Параллельное программирование в блоге о нативном коде