Поделиться через


Общие сведения о C++ AMP

Примечание.

Заголовки 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

Класс extent

Класс экстентов указывает длину данных в каждом измерении 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_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_view имеет почти те же члены, что array и класс, но базовое поведение не совпадает. Данные, передаваемые array_view конструктору, не реплицируются на GPU, так как это происходит с конструктором array . Вместо этого данные копируются в акселератор при выполнении функции ядра. Поэтому при создании двух объектов, использующих одни и array_view те же данные, оба array_view объекта ссылаются на одно и то же пространство памяти. При этом необходимо синхронизировать любой многопоточный доступ. Основное преимущество использования array_view класса заключается в том, что данные перемещаются только в том случае, если это необходимо.

Сравнение массива и array_view

В следующей таблице приведены общие сведения о сходствах и различиях между array ними и array_view классами.

Description array - класс array_view - класс
При определении ранга Во время компиляции. Во время компиляции.
Когда определяется экстент Во время выполнения. Во время выполнения.
Фигура Прямоугольный. Прямоугольный.
Хранилище данных Контейнер данных. Является оболочкой данных.
Копия Явное и глубокое копирование при определении. Неявное копирование при доступе к функции ядра.
Извлечение данных Путем копирования данных массива обратно в объект в поток ЦП. Путем прямого доступа к array_view объекту или путем вызова метода array_view::synchronize для продолжения доступа к данным в исходном контейнере.

Общая память с массивом и array_view

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

array Объекты можно использовать для указания точного управления использованием общей памяти, если связанный акселератор поддерживает его. Определяется ли акселератор поддержкой общей памяти свойством supports_cpu_shared_memory акселератора, которое возвращается true при поддержке общей памяти. Если общая память поддерживается, перечисление по умолчанию access_type для выделения памяти на акселераторе определяется свойствомdefault_cpu_access_type. По умолчанию объекты занимают то же самоеaccess_type, array что и array_view основной связанныйaccelerator.

Задав свойство array элемента данных массива::cpu_access_type свойству элемента данных явным образом, можно четко контролировать использование общей памяти, чтобы оптимизировать приложение для характеристик производительности оборудования на основе шаблонов доступа к памяти вычислительных ядер. Выражение array_view отражает то же cpu_access_type , что array оно связано; или, если array_view создается без источника данных, оно access_type отражает среду, которая сначала приводит к выделению хранилища. То есть, если он впервые обращается к узлу (ЦП), он ведет себя так, как если бы он был создан по источнику данных ЦП и access_type accelerator_view предоставляет общий доступ к связанному захвату, однако, если он впервые обращается к узлу, то он ведет себя так, как если бы он был создан над array созданным в этом accelerator_view случае и предоставляет общий доступ accelerator_viewк arrayнемуaccess_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_view объектеarray. Рассмотрим следующий код из введения этого раздела.

#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::p recise_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, doublefloatнорме или ненорме.

Приложения универсальной платформы Windows (UWP)

Как и другие библиотеки C++, вы можете использовать C++ AMP в приложениях UWP. В этих статьях описывается, как включить код C++ AMP в приложения, созданные с помощью C++, C#, Visual Basic или JavaScript:

Визуализатор C++ AMP и параллелизма

Визуализатор параллелизма включает поддержку анализа производительности кода C++ AMP. В этих статьях описаны следующие функции:

Рекомендации по производительности

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

См. также

C++ AMP (C++ Accelerated Massive Parallelism)
Синтаксис лямбда-выражений
Справочник (C++ AMP)
Параллельный программирование в блоге о машинном коде