Partilhar via


Visão geral do C++ AMP

Observação

Os cabeçalhos AMP C++ foram preteridos a partir do Visual Studio 2022 versão 17.0. A inclusão de cabeçalhos AMP gerará erros de compilação. Defina _SILENCE_AMP_DEPRECATION_WARNINGS antes de incluir quaisquer cabeçalhos AMP para silenciar os avisos.

C++ Accelerated Massive Parallelism (C++ AMP) acelera a execução de código C++ aproveitando o hardware paralelo de dados, como uma unidade de processamento gráfico (GPU) em uma placa gráfica discreta. Usando 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 AMP C++ inclui matrizes multidimensionais, indexação, transferência de memória, mosaico e uma biblioteca de funções matemáticas. Você pode usar extensões de linguagem C++ AMP para controlar como os dados são movidos da CPU para a GPU e vice-versa, para que você possa melhorar o desempenho.

Requisitos do sistema

  • Windows 7 ou posterior

  • Windows Server 2008 R2 até Visual Studio 2019.

  • Hardware DirectX 11 Feature Level 11.0 ou posterior

  • Para depuração no emulador de software, é necessário o Windows 8 ou Windows Server 2012. Para depuração no hardware, você deve instalar os drivers para sua placa gráfica. Para obter mais informações, consulte Debugging GPU Code.

  • Nota: Atualmente, o AMP não é suportado no ARM64.

Introdução

Os dois exemplos a seguir ilustram os componentes principais do C++ AMP. Suponha que você deseja adicionar os elementos correspondentes de duas matrizes unidimensionais. Por exemplo, talvez queira adicionar {1, 2, 3, 4, 5} e {6, 7, 8, 9, 10} para obter {7, 9, 11, 13, 15}. Sem usar 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 ciclo fornece um mecanismo para percorrer os elementos das matrizes. 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 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 construções AMP C++ são usadas:

  • Dados: Você usa matrizes C++ para construir três objetos array_view AMP C++. Você fornece quatro valores para construir um array_view objeto: os valores de dados, a classificação, o tipo de elemento e o array_view comprimento do 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 do construtor. Neste exemplo, a matriz C++ que é passada para o construtor é unidimensional. A ordem e o comprimento são usados para construir a forma retangular dos dados no objeto array_view, e os valores de dados são usados para preencher a matriz. A biblioteca de tempo de execução também inclui a Array Class, que tem uma interface semelhante à array_view classe e é discutida posteriormente neste artigo.

  • Iteração: A função parallel_for_each (C++ AMP) fornece um mecanismo para iterar sobre os elementos de dados, ou seja, o 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 apenas o subconjunto da linguagem C++ que o C++ AMP pode acelerar é usado.

  • Índice: A variável de classe de índice , 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.

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

Você deve definir os valores de dados e declarar a forma dos dados antes de 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 ser de qualquer tamanho em qualquer uma das dimensões.

Classe de índice

A classe index especifica uma posição no objeto array ou array_view, encapsulando o deslocamento a partir da origem em cada dimensão em um único objeto. Ao acessar um local na matriz, você 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 onde a linha = 1 e a coluna = 2 em um objeto bidimensional array_view . O primeiro parâmetro no construtor é o componente de linha index, 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 onde a profundidade = 0, a linha = 1 e a coluna = 3 em um objeto tridimensional array_view . Observe que o primeiro parâmetro é o componente profundidade, o segundo parâmetro é o componente linha e o terceiro parâmetro é o componente 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 classe extent especifica o comprimento dos dados em cada dimensão do array objeto 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 a dimensão de um array ou array_view objeto existente. O exemplo a seguir mostra 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 que o objeto no exemplo anterior, mas este exemplo usa um extent objeto em vez de usar parâmetros explícitos array_view no 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";

Transferir dados para o acelerador: array e array_view

Dois contêineres de dados usados para mover dados para o acelerador são definidos na biblioteca de tempo de execução. 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 do kernel acessa os dados. Quando os dados são necessários no dispositivo de origem, os dados são copiados de volta.

array Classe

Quando um array objeto é construído, uma cópia profunda dos dados é criada no acelerador se você usar um construtor que inclua um ponteiro para o conjunto de dados. A função kernel modifica a cópia no acelerador. Quando a execução da função do kernel estiver concluída, você deve 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 do kernel é concluída, o vector conversion operator é usado para copiar os dados de volta para o objeto vetorial.

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 Classe

O array_view tem quase os mesmos membros que a array classe, mas o comportamento subjacente não é o mesmo. Os dados passados para o array_view construtor não são replicados na GPU como acontece com um array construtor. Em vez disso, os dados são copiados para o acelerador quando a função do kernel é executada. Portanto, se você criar dois array_view objetos que usam os mesmos dados, ambos array_view os objetos se referem ao mesmo espaço de memória. Ao fazer isso, tens de sincronizar qualquer acesso em multithreading. A principal vantagem de usar a array_view classe é que os dados são movidos apenas se for necessário.

Comparação de matriz e array_view

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

Descrição classe de matriz 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.
Copiar Cópia explícita e profunda na definição. Cópia implícita quando é acessada pela função kernel.
Obtenção de dados Copiando os dados do array de volta para um objeto no thread da CPU. Por acesso direto ao array_view objeto ou chamando o método array_view::synchronize para continuar acessando os dados no contêiner original.

Memória compartilhada com array 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 de memória compartilhada se o acelerador associado oferecer suporte a ele. Se um acelerador suporta memória compartilhada é determinado pela propriedade supports_cpu_shared_memory do acelerador, que retorna true quando a memória compartilhada é suportada. Se houver suporte para memória compartilhada, a Enumeração padrão access_type para alocações de memória no acelerador será determinada pela propriedade default_cpu_access_type. Os objetos array e array_view assumem, por padrão, o mesmo access_type que o accelerator principal associado.

Ao definir a propriedade array::cpu_access_type Data Member de um array explicitamente, você pode exercer um controle refinado sobre como a memória compartilhada é usada, para que possa otimizar o aplicativo para as características de desempenho do hardware, com base nos padrões de acesso à memória de seus núcleos de computação. Um array_view reflete o mesmo cpu_access_type do array ao qual está associado; ou, se o array_view for construído sem uma fonte de dados, o seu access_type reflete o ambiente que primeiro provoca a alocação de armazenamento. Ou seja, se for acedido pela primeira vez pelo anfitrião (CPU), comporta-se como se tivesse sido criado sobre uma fonte de dados da CPU e partilha o access_type do accelerator_view associados por captura; no entanto, se for acedido pela primeira vez por um accelerator_view, comporta-se como se tivesse sido criado sobre um array criado nesse accelerator_view e partilha o array do access_type.

O exemplo de código a seguir mostra como determinar se o acelerador padrão oferece 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 se pretende executar no acelerador em relação aos 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. Neste 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 neste 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 apenas o subconjunto da linguagem C++ que o C++ AMP pode acelerar é usado. As limitações das funções que têm o modificador restrict são descritas em restrict (C++ AMP). Para obter mais informações, consulte Sintaxe de expressão do 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";
    }
}

Código de Aceleração: Telhas e Barreiras

Você pode obter aceleração adicional usando o tiling. O ladrilho divide os fios em subconjuntos retangulares iguais ou azulejos. Você determina o tamanho apropriado do bloco com base no conjunto de dados e no algoritmo que está codificando. Para cada thread, tem acesso à localização global de um elemento de dados relativo ao todo array ou array_view e acesso à localização local relativa ao bloco. Usar o 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 tiling, chame o método extent::tile no domínio de computação no parallel_for_each método e use 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 tem que acessar e acompanhar os valores em todo o bloco. Use a palavra-chave tile_static Keyword 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 manipular a sincronização do acesso de tile-thread à variável. O método tile_barrier::wait interrompe a execução do thread atual até que todos os threads no tile tenham chegado à chamada para tile_barrier::wait. Assim, é possível 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 que é 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 de Matemática

O C++ AMP inclui duas bibliotecas matemáticas. A biblioteca de precisão dupla no namespace Concurrency::precise_math fornece suporte para funções de precisão dupla. 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. Está em conformidade com a especificação C99 (ISO/IEC 9899). O acelerador deve suportar uma precisão dupla total. Pode verificar se isso ocorre verificando o valor do atributo 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 suportam apenas float 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 ficheiro de cabeçalho <amp_math.h> e todas são declaradas com restrict(amp). As funções no <cabeçalho cmath> são importadas para os namespaces fast_math e precise_math. A restrict palavra-chave é usada para distinguir a <versão cmath> e a versão C++ AMP. O código a seguir calcula o logaritmo de base-10, usando o método fast, 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 gráficos

O C++ AMP inclui uma biblioteca de gráficos projetada para programação gráfica acelerada. Esta biblioteca é utilizada apenas em dispositivos que suportam a funcionalidade gráfica nativa. 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 a partir 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++ no que diz respeito à atribuição e à construção de cópias. Para obter mais informações, consulte C++ Standard Library Containers. 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: Fornece acesso apenas 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, double, norm ou unorm.

Aplicativos da Plataforma Universal do Windows (UWP)

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

C++ AMP e Visualizador de Simultaneidade

O Visualizador de Simultaneidade inclui suporte para analisar o desempenho do código AMP C++. Estes artigos descrevem estas características:

Performance Recommendations (Recomendações de Desempenho)

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

Ver também

C++ AMP (paralelismo maciço acelerado em C++)
Sintaxe da expressão do Lambda
Referência (C++ AMP)
Programação paralela no blog Native Code