Compartilhar via


Visão geral do C++ AMP

Observação

Os cabeçalhos C++ AMP foram preteridos a partir do Visual Studio 2022 versão 17.0. Incluir todos os cabeçalhos AMP gerará erros de build. Defina _SILENCE_AMP_DEPRECATION_WARNINGS antes de incluir qualquer cabeçalho AMP para silenciar os avisos.

O C++ Accelerated Massive Parallelism (C++ AMP) acelera a execução do código C++ aproveitando o hardware paralelo de dados, como uma GPU (unidade de processamento gráfico) em uma placa gráfica discreta. Usando o C++ AMP, você pode codificar algoritmos de dados multidimensionais para que a execução possa ser acelerada usando paralelismo em hardware heterogêneo. O modelo de programação C++ AMP inclui matrizes multidimensionais, indexação, transferência de memória, tilização e uma biblioteca para funções matemáticas. Você pode usar extensões de linguagem AMP do C++ para controlar como os dados são movidos da CPU para a GPU e voltar, para que você possa melhorar o desempenho.

Requisitos do Sistema

  • Windows 7 ou posterior

  • Windows Server 2008 R2 até Visual Studio 2019.

  • Hardware do DirectX 11 Feature Level 11.0 ou posterior

  • Para depuração no emulador de software, o Windows 8 ou o Windows Server 2012 são necessários. Para depuração no hardware, você deve instalar os drivers para sua placa gráfica. Para obter mais informações, confira Depurando Código GPU.

  • Observação: atualmente, não há suporte para AMP no ARM64.

Introdução

Os dois exemplos a seguir ilustram os principais componentes do AMP C++. Suponha que você queira adicionar os elementos correspondentes de duas matrizes unidimensionais. Por exemplo, talvez você queira adicionar {1, 2, 3, 4, 5} e {6, 7, 8, 9, 10} para obter {7, 9, 11, 13, 15}. Sem usar o C++ AMP, você pode escrever o código a seguir para adicionar os números e exibir os resultados.

#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";
    }
}

As partes importantes do código são as seguintes:

  • Dados: os dados consistem em três matrizes. Todos têm a mesma classificação (um) e comprimento (cinco).

  • Iteração: o primeiro for loop fornece um mecanismo para iterar através dos elementos nos arrays. O código que você deseja executar para calcular as somas está contido no primeiro for bloco.

  • Índice: A idx variável acessa os elementos individuais das matrizes.

Usando o C++ AMP, você pode escrever o código a seguir.

#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";
    }
}

Os mesmos elementos básicos estão presentes, mas os constructos AMP do C++ são usados:

  • Dados: você usa matrizes C++ para construir três objetos array_view C++ AMP. Você fornece quatro valores para construir um array_view objeto: os valores de dados, a classificação, o tipo de elemento e o comprimento do array_view objeto em cada dimensão. A classificação e o tipo são passados como parâmetros de tipo. Os dados e o comprimento são passados como parâmetros de construtor. Neste exemplo, a matriz C++ que é passada para o construtor é unidimensional. A classificação e o comprimento são usados para construir a forma retangular dos dados no array_view objeto e os valores de dados são usados para preencher a matriz. A biblioteca de runtime também inclui a Classe de matriz, que tem uma interface que se assemelha à array_view classe e é discutida posteriormente neste artigo.

  • Iteração: a função parallel_for_each (C++ AMP) fornece um mecanismo para percorrer os elementos de dados ou domínio de computação. Neste exemplo, o domínio de computação é especificado por sum.extent. O código que você deseja executar está contido em uma expressão lambda ou função kernel. O restrict(amp) indica que é usado somente o subconjunto da linguagem C++ que o C++ AMP pode acelerar.

  • Índice: A variável classe index , idxé declarada com uma classificação de um para corresponder à classificação do array_view objeto. Usando o índice, você pode acessar os elementos individuais dos array_view objetos.

Modelagem e indexação de dados: índice e extensão

Você deve definir os valores de dados e declarar a forma dos dados antes de poder executar o código do kernel. Todos os dados são definidos como uma matriz (retangular) e você pode definir a matriz para ter qualquer classificação (número de dimensões). Os dados podem ter qualquer tamanho em qualquer uma das dimensões.

Classe index

A Classe index especifica um local no objeto array ou array_view ao encapsular o deslocamento a partir da origem em cada dimensão em um único objeto. Quando você acessa um local na matriz, passa um index objeto para o operador de indexação, []em vez de uma lista de índices inteiros. Você pode acessar os elementos em cada dimensão usando o operador array::operator() ou o operador array_view::operator().

O exemplo a seguir cria um índice unidimensional que especifica o terceiro elemento em um objeto unidimensional array_view . O índice é usado para imprimir o terceiro elemento no array_view objeto. A saída é 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

O exemplo a seguir cria um índice bidimensional que especifica o elemento em que a linha = 1 e a coluna = 2 em um objeto bidimensional array_view . O primeiro parâmetro no index construtor é o componente de linha e o segundo parâmetro é o componente de coluna. A saída é 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

O exemplo a seguir cria um índice tridimensional que especifica o elemento em que a profundidade = 0, a linha = 1 e a coluna = 3 em um objeto tridimensional array_view . Observe que o primeiro parâmetro é o componente de profundidade, o segundo parâmetro é o componente de linha e o terceiro parâmetro é o componente de coluna. A saída é 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

Classe de extensão

A extent Class especifica o comprimento dos dados em cada dimensão do objeto array ou array_view. Você pode criar uma extensão e usá-la para criar um array ou array_view objeto. Você também pode recuperar os atributos ou a extensão de um array ou array_view objeto existente. O exemplo a seguir imprime o tamanho da extensão em cada dimensão de um objeto 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";

O exemplo a seguir cria um array_view objeto que tem as mesmas dimensões do objeto no exemplo anterior, mas este exemplo usa um extent objeto em vez de usar parâmetros explícitos no array_view construtor.

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";

Movendo dados para o acelerador: matriz e array_view

Dois contêineres de dados usados para mover dados para o acelerador são definidos na biblioteca de runtime. Eles são a classe de matriz e a classe array_view. A array classe é uma classe de contêiner que cria uma cópia profunda dos dados quando o objeto é construído. A array_view classe é uma classe wrapper que copia os dados quando a função kernel acessa os dados. Quando os dados são necessários no dispositivo de origem, os dados são copiados novamente.

Classe array

Quando um array objeto é construído, uma cópia profunda dos dados é criada no acelerador se você usa um construtor que inclui um ponteiro para o conjunto de dados. A função kernel modifica a cópia que está no acelerador. Quando a execução da função kernel for concluída, você deverá copiar os dados de volta para a estrutura de dados de origem. O exemplo a seguir multiplica cada elemento em um vetor por 10. Depois que a função kernel for concluída, o vector conversion operator será utilizado para copiar os dados de volta para o objeto de vetor.

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";
}

Classe array_view

O array_view tem quase os mesmos membros da array classe, mas o comportamento subjacente não é o mesmo. Os dados fornecidos ao construtor array_view não são replicados na GPU como ocorre com o construtor array. Em vez disso, os dados são copiados para o acelerador quando a função kernel é executada. Portanto, se você criar dois array_view objetos que usam os mesmos dados, ambos os array_view objetos se referirão ao mesmo espaço de memória. Ao fazer isso, você precisa sincronizar todo e qualquer acesso multithread. A principal vantagem de usar a classe é que os array_view dados são movidos somente se necessário.

Comparação de matriz e array_view

A tabela a seguir resume as semelhanças e diferenças entre as classes array e array_view.

Descrição Classe de array classe array_view
Quando a classificação é determinada Em tempo de compilação. Em tempo de compilação.
Quando a extensão é determinada Em tempo de execução. Em tempo de execução.
Forma Retangular. Retangular.
Armazenamento de dados É um contêiner de dados. É um envoltório de dados.
Copy Cópia explícita e profunda na definição. Cópia implícita quando é acessada pela função kernel.
Recuperação de dados Copiando os dados da matriz de volta para um objeto no thread da CPU. Por meio do acesso direto do array_view objeto ou chamando o método array_view::synchronize para continuar acessando os dados no contêiner original.

Memória compartilhada com matriz e array_view

A memória compartilhada é a memória que pode ser acessada pela CPU e pelo acelerador. O uso de memória compartilhada elimina ou reduz significativamente a sobrecarga de copiar dados entre a CPU e o acelerador. Embora a memória seja compartilhada, ela não pode ser acessada simultaneamente pela CPU e pelo acelerador e isso causa um comportamento indefinido.

array os objetos podem ser usados para especificar o controle refinado sobre o uso da memória compartilhada se o acelerador associado der suporte a ele. Se um acelerador dá suporte à memória compartilhada é determinado pela propriedade supports_cpu_shared_memory do acelerador, que retorna true quando há suporte para memória compartilhada. Se houver suporte para memória compartilhada, o padrão access_type Enumeração para alocações de memória no acelerador será determinado pela default_cpu_access_type propriedade. Por padrão, array e array_view objetos assumem o mesmo access_type que o primário associado accelerator.

Definindo a propriedade array::cpu_access_type Data Member de forma array explícita, você pode exercer um controle refinado sobre como a memória compartilhada é usada, para que você possa otimizar o aplicativo para as características de desempenho do hardware, com base nos padrões de acesso à memória de seus kernels de computação. Um array_view reflete o mesmo cpu_access_type que o array associado a ele; ou, se o array_view for construído sem uma fonte de dados, ele access_type refletirá o ambiente que primeiro faz com que ele aloque o armazenamento. Ou seja, se ele for acessado pela primeira vez pela CPU (host), ele se comportará como se tivesse sido criado em uma fonte de dados de CPU e compartilha o access_type do accelerator_view associado pela captura; no entanto, se ele for acessado pela primeira vez por um accelerator_view, ele se comportará como se tivesse sido criado em um array criado nesse accelerator_view e compartilha o array do access_type.

O exemplo de código a seguir mostra como determinar se o acelerador padrão dá suporte à memória compartilhada e, em seguida, cria várias matrizes que têm configurações de cpu_access_type diferentes.

#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);
}

Executando código sobre dados: parallel_for_each

A função parallel_for_each define o código que você deseja executar no acelerador sobre os dados no objeto array ou array_view. Considere o código a seguir da introdução deste tópico.

#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";
    }
}

O parallel_for_each método usa dois argumentos, um domínio de computação e uma expressão lambda.

O domínio de computação é um extent objeto ou um tiled_extent objeto que define o conjunto de threads a serem criados para execução paralela. Um thread é gerado para cada elemento no domínio de computação. Nesse caso, o extent objeto é unidimensional e tem cinco elementos. Portanto, cinco threads são iniciados.

A expressão lambda define o código a ser executado em cada thread. A cláusula [=]de captura especifica que o corpo da expressão lambda acessa todas as variáveis capturadas por valor, que, nesse caso, são a, be sum. Neste exemplo, a lista de parâmetros cria uma variável unidimensional index chamada idx. O valor do idx[0] é 0 no primeiro thread e aumenta em um em cada thread subsequente. O restrict(amp) indica que é usado somente o subconjunto da linguagem C++ que o C++ AMP pode acelerar. As limitações em funções que têm o modificador de restrição são descritas em restrição (C++ AMP). Para obter mais informações, consulte a sintaxe de expressão Lambda.

A expressão lambda pode incluir o código a ser executado ou pode chamar uma função de kernel separada. A função kernel deve incluir o restrict(amp) modificador. O exemplo a seguir é equivalente ao exemplo anterior, mas chama uma função de kernel separada.

#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";
    }
}

Acelerando o código: blocos e barreiras

Você pode obter aceleração adicional usando blocos. A divisão em azulejos divide os fios de execução em subconjuntos retangulares iguais ou azulejos. Você determina o tamanho do bloco apropriado com base no conjunto de dados e no algoritmo que você está codificando. Para cada thread, você tem acesso à localização global de um elemento de dados relativo ao todo array ou array_view e acesso à localização local em relação ao bloco. O uso do valor de índice local simplifica seu código porque você não precisa escrever o código para traduzir valores de índice de global para local. Para usar a técnica de divisão em blocos, chame o método extent::tile no domínio de computação dentro do método parallel_for_each, e utilize um objeto tiled_index na expressão lambda.

Em aplicativos típicos, os elementos em um bloco estão relacionados de alguma forma, e o código precisa acessar e controlar os valores em todo o bloco. Use a palavra-chave tile_static e o método tile_barrier::wait para fazer isso. Uma variável que tem a palavra-chave tile_static tem um escopo em um bloco inteiro e uma instância da variável é criada para cada bloco. Você deve lidar com a sincronização do acesso de bloco-thread à variável. O método tile_barrier::wait interrompe a execução do thread atual até que todos os threads no tile tenham atingido a chamada para tile_barrier::wait. Portanto, você pode acumular valores no bloco usando variáveis tile_static. Em seguida, você pode concluir quaisquer cálculos que exijam acesso a todos os valores.

O diagrama a seguir representa uma matriz bidimensional de dados de amostragem organizada em blocos.

Valores de índice em uma extensão em mosaico.

O exemplo de código a seguir usa os dados de amostragem do diagrama anterior. O código substitui cada valor no bloco pela média dos valores no bloco.

// 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

Bibliotecas matemáticas

O C++ AMP inclui duas bibliotecas matemáticas. A biblioteca de dupla precisão no Namespace Concurrency::precise_math fornece suporte para funções de dupla precisão. Ele também fornece suporte para funções de precisão única, embora o suporte de precisão dupla no hardware ainda seja necessário. Ele está em conformidade com a Especificação C99 (ISO/IEC 9899). O acelerador deve dar suporte a precisão dupla total. Você pode determinar se ele faz isso verificando o valor do membro de dados accelerator::supports_double_precision. A biblioteca de matemática rápida, no Namespace Concurrency::fast_math, contém outro conjunto de funções matemáticas. Essas funções, que dão suporte apenas float a operandos, são executadas mais rapidamente, mas não são tão precisas quanto as da biblioteca matemática de precisão dupla. As funções estão contidas no <arquivo de cabeçalho amp_math.h> e todas são declaradas com restrict(amp). As funções no <arquivo de cabeçalho cmath> são importadas nos namespaces fast_math e precise_math. A restrict palavra-chave é usada para distinguir a <versão cmath> e a versão do C++ AMP. O código a seguir calcula o logaritmo de base 10, usando o método rápido, de cada valor que está no domínio de computação.

#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";
    }
}

Biblioteca de elementos gráficos

O C++ AMP inclui uma biblioteca de gráficos projetada para programação gráfica acelerada. Essa biblioteca é usada somente em dispositivos que dão suporte à funcionalidade de elementos gráficos nativos. Os métodos estão no namespace Concurrency::graphics e estão contidos no <arquivo de cabeçalho amp_graphics.h> . Os principais componentes da biblioteca de gráficos são:

  • Classe de textura: você pode usar a classe de textura para criar texturas da memória ou de um arquivo. As texturas se assemelham a matrizes porque contêm dados e se assemelham a contêineres na Biblioteca Padrão C++ em relação à atribuição e à construção da cópia. Para obter mais informações, consulte Contêineres da Biblioteca Padrão do C++. Os parâmetros de modelo para a texture classe são o tipo de elemento e a classificação. A classificação pode ser 1, 2 ou 3. O tipo de elemento pode ser um dos tipos de vetor curtos descritos posteriormente neste artigo.

  • classe writeonly_texture_view: oferece acesso exclusivo para gravação a qualquer textura.

  • Biblioteca de Vetores Curtos: define um conjunto de tipos de vetores curtos de comprimento 2, 3 e 4 que se baseiam em int, uint, , float, doublenorma ou unorm.

Aplicativos da Plataforma Universal do Windows (UWP)

Assim como outras bibliotecas C++, você pode usar o AMP C++ em seus aplicativos UWP. Estes artigos descrevem como incluir código AMP C++ em aplicativos criados usando C++, C#, Visual Basic ou JavaScript:

Visualizador de AMP e Simultaneidade do C++

A Visualização Simultânea inclui suporte para analisar o desempenho do código C++ AMP. Estes artigos descrevem estes recursos:

Recomendações de desempenho

O módulo e a divisão de inteiros sem sinal têm um desempenho significativamente melhor do que o módulo e a divisão de inteiros assinados. Recomendamos que você use números inteiros sem sinal quando possível.

Consulte também

C++ AMP (Paralelismo Massivo Acelerado C++)
Sintaxe da expressão Lambda
Referência (C++ AMP)
Programação paralela no blog de código nativo