Compartilhar via


Visão geral do C++ AMP

C++ Accelerated Massive Parallelism (C++ AMP, em inglês) acelera a execução do código C++ se aproveitando do hardware de dados paralelo como unidade de processamento gráfico (GPU, em inglês) em uma placa gráfica discreta. Usando o C++ AMP, você pode codificar algoritmos de dados multi-dimensionais de modo que a execução possa 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 a forma 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 de Recursos 11.0 do DirectX 11 ou hardware posterior

  • 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 Depurando código de 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.

#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 as construções C++ AMP são usadas:

  • 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 em tempo de execução também inclui a Classe array, que tem uma interface que se assemelha à classe array_view e é abordada posteriormente neste artigo.

  • Iteração: a Função parallel_for_each (C++ AMP) 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 de kernel. O restrict(amp) indica que apenas o subconjunto da linguagem C++ que o C++ AMP pode acelerar é usado.

  • Índice: a variável Classe index , 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 serem 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

O Classe index 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 array::operator() ou Operador 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

classe de extensão

O Classe extent (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 tem 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 em tempo de execução. Eles são o Classe array e o Classe array_view. 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ória que copia os dados quando a função de kernel acessa os dados. Quando os dados são necessários no dispositivo de origem, os dados são copiados de volta.

classe array

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 de kernel modifica a cópia no acelerador. Quando a execução da função de kernel for concluída, você deverá 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 de 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";
}

Classe 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 são replicados na GPU como são com um construtor array. Em vez disso, os dados são copiados para o acelerador quando a função de 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ê fizer isso, será necessário sincronizar qualquer acesso multissegmentado. A principal vantagem de usar a classe array_view é que os dados são movidos somente se necessário.

Comparaçã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 é 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 de kernel.

Recuperação de dados

Copiando os dados da matriz de volta para um objeto no segmento da CPU.

Por acesso direto do objeto array_view ou chamando Método array_view::synchronize para continuar a acessar os dados no contêiner original.

Memória compartilhada com matriz e array_view

A memória compartilhada é a 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, não pode ser acessada simultaneamente pela CPU e pelo acelerador, e fazendo isso provocaria um comportamento indefinido.

Os objetos de array podem ser usados para especificar o controle mais aguçado sobre o uso de memória compartilhada se o acelerador associado oferecer suporte a ele. Se um acelerador oferece ou não suporte à memória compartilhada, é determinado pela propriedade de supports_cpu_shared_memory de aceleradores, que retorna true quando a memória compartilhada é suportada. Se a memória compartilhada tiver suporte, Enumeração access_type padrão para as alocações de memória no acelerador será determinado pela propriedade default_cpu_access_type. Por padrão, array e objetos de array_view recebem no mesmo access_type que o acceleratorassociado primário.

Definindo a propriedade de Membro de Dados array::cpu_access_type de array explicitamente, você pode exercitar um controle mais aguçado sobre como a memória compartilhada é usada, de modo a otimizar o aplicativo para as características de desempenho de hardware, com base em padrões de acesso de memória dos kernels de computação. array_view reflete o mesmo cpu_access_type que a array com a qual está associado; ou, se o array_view for construído sem uma fonte de dados, o access_type reflete o ambiente que faz com que ele primeiro atribua o armazenamento. Isto é, se for acessada primeiro pelo host (CPU), ela se comportará como se tivesse sido criada em uma fonte de dados de CPU e compartilha o access_type da accelerator_view associada por captura. No entanto, se for acessada primeiro por uma accelerator_view, ela se comportará como se tivesse sido criada em uma array criada nessa accelerator_view e compartilha o access_type da array.

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 diferentes configurações de 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);
}

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 segmentos a ser criado para a execução paralela. Um segmento é gerado para cada elemento no domínio do cálculo. Nesse caso, o objeto extent é unidimensional e tem cinco elementos. Portanto, cinco segmentos são iniciados.

A expressão lambda define o código a ser executado em cada segmento. A cláusula de captura [=], especifica que o corpo da expressão lambda acessa todas as variáveis capturadas pelo 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 segmento e aumenta um em cada segmento 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 da expressão lambda.

A expressão lambda pode incluir o código para executar ou pode chamar uma função de kernel separada. A função de kernel deve incluir o modificador restrict(amp). 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 Código: Blocos e Barreiras

Você pode obter aceleração adicional usando a disposição lado a lado. A disposição lado a lado divide os segmentos em subconjuntos retangulares iguais ou blocos (tiles, em inglês). Você determina o tamanho apropriado para o bloco baseado no seu conjunto de dados e no algoritmo que você está codificando. Para cada segmento, 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 relativa 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 Método extent::tile 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 dos valores através do bloco. Use a palavra-chave palavra-chave de tile_static e o Método 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 de segmento de bloco para a variável. O Método tile_barrier::wait para a execução do segmento atual até que todos os segmentos 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 é organizada em blocos.

Valores de índice em uma extensão 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

O C++ AMP inclui duas bibliotecas matemáticas. A biblioteca de precisão dupla em Namespace Concurrency::precise_math fornece suporte a 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. Está em conformidade com 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 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 de <cmath> e a versão do C++ AMP. O código a seguir calcula o logaritmo 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 gráficos

O 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 às 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 texture: 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 aos recipientes 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 Contêineres da STL. 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.

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

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

Aplicativos do Windows Store

Como em outras bibliotecas C++, você pode usar o C++ AMP em seus aplicativos do Windows Store. Esses artigos descrevem como incluir o código C++ AMP em aplicativos 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 da expressão lambda

Outros recursos

C++ AMP (C++ Accelerated Massive Parallelism)

Referência (C++ AMP)

Blog Programação Paralela no Código Nativo