Partilhar via


Visão geral do C++ AMP

Paralelismo Maciço Acelerado do C++ (C++ AMP, em inglês) acelera a execução do código C++ se aproveitando de hardware de dados paralelo como unidade de processamento gráficos (GPU, em inglês) em uma placa gráfica discreta.Usando o C++ AMP, você pode codificar algoritmos de dados de multi-dimensionais de modo que a execução pode ser acelerada usando o paralelismo em hardware heterogêneo.O modelo de programação de C++ AMP inclui matrizes multidimensionais, indexação, transferência de memória, disposição lado-a-lado, e uma biblioteca de funções matemáticas.Você pode usar extensões de idioma do 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, Windows 8, Windows Server 2008 R2, ou Windows Server 2012

  • Nível 11,0 do recurso de DirectX 11 ou posterior hardware

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

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, você pode querer 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 de código são:

  • Dados: Os dados consistem em três matrizes.Todas têm a mesma fila (uma) e comprimento (cinco).

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

  • Índice: A variável idx 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 são usadas construções C++ AMP:

  • Dados: Você usa matrizes C++ para construir três objetos array_view do C++ AMP.Você fornece quatro valores para construir um objeto array_view: os valores de dados, a classificação, o tipo do elemento, e o comprimento do objeto array_view 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.Nesse 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 objeto array_view , e os valores dos dados são usados para preencher a matriz.A biblioteca tempo de execução também inclui a Classe Array, que tem uma interface que se assemelha a classe array_view e é abordada posteriormente neste artigo.

  • Iteração: A parallel_for_each função (AMP C++) fornece um mecanismo para iteração através dos elementos de dados, ou do domínio do cálculo.Nesse exemplo, o domínio de cálculo é 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 índice de classe , idx, é declarada com a classificação um para corresponder à classificação do objeto array_view.Usando o índice, você pode acessar elementos individuais dos objetos array_view.

Modelando e Indexando 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 kernel.Todos os dados são definidos para ser 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.

Hh265136.collapse_all(pt-br,VS.110).gifClasse de índice

O índice de classe especifica um local no objeto array ou array_view encapsulando o deslocamento da fonte em cada dimensão em um objeto.Quando você acessa um local na matriz, você passa um objeto index para o operador de indexação, [], em vez de uma lista de índices inteiros.Você pode acessar os elementos em cada dimensão usando Operador de array::Operator() ou Operador de array_view::Operator().

O exemplo a seguir cria um índice unidimensional que especifica o terceiro elemento em um objeto array_view unidimensional.O índice é usado para imprimir o terceiro elemento no objeto array_view.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 coluna = 2 em um objeto bidimensional array_view.O primeiro parâmetro no construtor index é o componente da linha, e o segundo parâmetro é o componente da 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 profundidade = 0, linha = 1, e coluna = 3 em um objeto array_view tridimensional.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

Hh265136.collapse_all(pt-br,VS.110).gifClasse de extensão

O extensão de classe (C++ AMP) 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 objeto array ou array_view.Você também pode recuperar a extensão de um objeto array ou array_view existente.O exemplo a seguir imprime o comprimento 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 seguinte exemplo cria um objeto array_view que tenha as mesmas dimensões que o objeto no exemplo anterior, mas este exemplo usa um objeto extent em vez de usar parâmetros explícitos no construtor 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";

Movendo Dados para o acelerador: array e array_view

Dois contêineres de dados usados para mover dados ao acelerador são definidos na biblioteca tempo de execução.Eles são o Classe Array e o array_view classe.A classe array é uma classe recipiente que cria uma cópia profunda dos dados quando o objeto é construído.A classe array_view é uma classe envoltório 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 de volta.

Hh265136.collapse_all(pt-br,VS.110).gifClasse de matriz

Quando um objeto array é construído, uma cópia profunda dos dados é criada no acelerador se você usar um construtor que inclui um ponteiro para o conjunto de dados.A função kernel modifica a cópia no acelerador.Quando a execução da função kernel é concluída, você deve copiar os dados de volta para a estrutura da fonte de dados.O exemplo a seguir multiplica cada elemento em um vetor por 10.Depois que a função kernel é concluída, o operador de conversão vetorial é usado para copiar os dados de volta no objeto 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";
}

Hh265136.collapse_all(pt-br,VS.110).gifClasse array_view

O array_view tem quase os mesmos membros que a classe array, mas o comportamento subjacente é diferente.Dados passados para o construtor array_view não estão replicados na GPU como são com um construtor array.Em vez de isso, os dados são copiados para o acelerador quando a função kernel é executada.Portanto, se você criar dois objetos array_view que usam os mesmos dados, ambos os objetos array_view referem-se ao mesmo espaço de memória.Quando você faz isso, você precisa sincronizar qualquer acesso multissegmentado.A principal vantagem de usar a classe array_view é que os dados são movidos somente se necessário.

Hh265136.collapse_all(pt-br,VS.110).gifComparação entre array e array_view

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

Descrição

Classe array

Classe array_view

Quando a classificação está 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 invólucro de dados.

Copiar

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 acesso direto do objeto array_view ou chamando Método de array_view::Synchronize para continuar a acessar os dados no contêiner original.

Executando o Código sobre Dados: parallel_for_each

A função parallel_for_each define o código que você deseja executar no acelerador contra os dados no objeto array ou array_view.Considere o seguinte código 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 método parallel_for_each recebe dois argumentos, um domínio de cálculo e uma expressão lambda.

O domínio de cálculo é um objeto extent ou tiled_extent que define o conjunto de threads a criar para a execução paralela.Uma thread é gerado para cada elemento no domínio do cálculo.Nesse caso, o objeto extent é unidimensional e tem cinco elementos.Portanto, cinco threads são iniciadas.

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, b, e sum.Nesse exemplo, a lista de parâmetros cria uma variável index unidimensional chamada idx.O valor do idx[0] é 0 no primeiro thread e aumenta um em cada thread subsequente.O restrict(amp) indica que apenas o subconjunto da linguagem C++ que o C++ AMP pode acelerar é usado.As restrições em funções que têm o modificador de restrição são descritas em Cláusula de restrição (AMP C++).Para obter mais informações, consulte Sintaxe de expressões lambda.

A expressão lambda pode incluir o código para executar ou pode chamar uma função kernel separada.A função kernel deve incluir o modificador restrict(amp).O exemplo a seguir é equivalente ao exemplo anterior, mas chama uma função 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 Código: Blocos e Barreiras

Você pode obter aceleração adicional usando disposição lado a lado.Disposição lado a lado divide os threads em subconjuntos retangulares iguais ou blocos (tiles, em inglês).Você determinam o tamanho apropriado para o bloco baseado no seu conjunto de dados e no algoritmo que você está codificando.Para cada thread, você tem acesso ao local global de um elemento de dados relativo a todo o array ou array_view e acesso a localização local relativo ao bloco.Usar o valor de índice local simplifica o seu código porque você não tem que escrever código para converter valores de índice de global para local.Para usar disposição lado a lado, chame o extensão::organizar lado a lado método no domínio de cálculo no método parallel_for_each, e use um objeto tiled_index na expressão lambda.

Em aplicações típicas, os elementos em um bloco são relacionados de alguma maneira, e o código precisa acessar e manter controle de valores através do bloco.Use a palavra-chave palavra-chave de tile_static e o Método de tile_barrier::wait para completar isso.Uma variável que tenha a palavra-chave tile_static tem um escopo através de um bloco inteiro, e uma instância da variável é criada para cada bloco.Você deve lidar com a sincronização do acesso thread do bloco para a variável.O Método de tile_barrier::wait para a execução do thread atual até que todos os threads no bloco tenham atingido a chamada para tile_barrier::wait.Assim você pode acumular valores através do bloco usando variáveis tile_static.Em seguida você pode concluir qualquer cálculo que exija acesso a todos os valores.

O diagrama a seguir representa uma matriz bidimensional de dados de amostragem que são organizados em blocos.

Valores de índice em uma extensão de lado a lado

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

C++ AMP inclui duas bibliotecas matemáticas.A biblioteca de precisão dupla em Namespace Concurrency::precise_math fornece suporte para funções de precisão dupla.Também fornece suporte para funções de precisão simples, embora o suporte de precisão dupla no hardware ainda seja necessário.Ela cumpre a especificação C99 (ISO/IEC 9899).O acelerador deve oferecer suporte completo à precisão dupla.Você pode determinar se ele tem verificando o valor do Membro de dados de Accelerator::supports_double_precision.A biblioteca de matemática rápida, no Namespace Concurrency::fast_math, contém um outro conjunto de funções matemáticas.Essas funções, que suportam apenas operandos float executam mais rapidamente, mas não são tão precisas quanto aquelas na 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 em ambos os namespaces fast_math e precise_math.A palavra-chave restrict é usada para distinguir a versão <cmath> da versão do C++ AMP. O código a seguir calcula o logaritmo na base 10, usando o método rápido, de cada valor que está no domínio de cálculo.

#include <amp.h>
#include <amp_math.h>
#include <iostream>
using namespace concurrency;


void MathExample() {

    double numbers[] = { 1.0, 10.0, 60.0, 100.0, 600.0, 1000.0 };
    array_view<double, 1> logs(6, numbers);

    parallel_for_each(
        logs.extent,
         [=] (index<1> idx) restrict(amp) {
            logs[idx] = concurrency::fast_math::log10(logs[idx]);
        }
    );

    for (int i = 0; i < 6; i++) {
        std::cout << logs[i] << "\n";
    }
}

Biblioteca de Elementos Gráficos

C++ AMP inclui uma biblioteca de elementos gráficos que é criada para a programação acelerada de elementos gráficos.Esta biblioteca é usada apenas em dispositivos que oferecem suporte nativo as funcionalidades dos elementos gráficos.Os métodos estão no Namespace Concurrency::Graphics e estão contidos no arquivo de cabeçalho <amp_graphics.h>.Os componentes chave de biblioteca de elementos gráficos são:

  • Classe de textura.: Você pode usar a classe texture para criar texturas da memória ou de um arquivo.As texturas são semelhantes a matrizes porque elas contêm dados, e são semelhantes a recipiente na Biblioteca de Modelo Padrão (STL, em inglês) no que diz respeito a atribuição e construção de cópia.Para obter mais informações, consulte STL Containers.Os parâmetros de modelo para a classe texture 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 curto que são descritos mais adiante neste artigo.

  • writeonly_texture_view classe: Fornece acesso somente escrita para qualquer textura.

  • Biblioteca de vetor curto: Define um conjunto de tipos vetor curto de comprimento 2, 3 e 4, que são baseados em int, uint, float, double, norm, ou unorm.

aplicativos deWindows Store

Como em outras bibliotecas C++, você pode usar o C++ AMP em seus apps do Windows Store.Esses artigos descrevem como incluir o código C++ AMP em apps que são criados usando C++, C#, Visual Basic ou Javascript:

C++ AMP e visualizador de concorrência

O visualizador de concorrência inclui suporte para analisar o desempenho do código C++ AMP.Esses artigos descrevem esses recursos:

Recomendações de Desempenho

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

Consulte também

Referência

Sintaxe de expressões lambda

Outros recursos

AMP C++ (C++ acelerado paralelismo maciço)

Referência (AMP C++)

Blog de Programação Paralela em Código Nativo