Partilhar via


Visão Geral do AMP em C++

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.

O Paralelismo Massivo Acelerado C++ (C++ AMP) acelera a execução de código C++ ao tirar partido de hardware paralelo de dados, como uma unidade de processamento gráfico (GPU) numa placa gráfica discreta. Ao usar C++ AMP, pode programar algoritmos de dados multidimensionais para que a execução possa ser acelerada usando paralelismo em hardware heterogéneo. O modelo de programação AMP em C++ inclui arrays multidimensionais, indexação, transferência de memória, mosaico e uma biblioteca de funções matemáticas. Podes usar extensões da linguagem C++ AMP para controlar como os dados são movidos da CPU para a GPU e vice-versa, para que possas melhorar o desempenho.

Requisitos do sistema

  • Windows 7 ou posterior

  • Windows Server 2008 R2 até Visual Studio 2019.

  • Hardware do DirectX 11 Nível de Funcionalidade 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: O AMP atualmente não é suportado no ARM64.

Introduction

Os dois exemplos seguintes ilustram os componentes principais do C++ AMP. Suponha que quer adicionar os elementos correspondentes de dois arrays unidimensionais. Por exemplo, pode querer adicionar {1, 2, 3, 4, 5} e {6, 7, 8, 9, 10} para obter {7, 9, 11, 13, 15}. Sem usar C++ AMP, pode escrever o seguinte código para somar os números e mostrar 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 arrays. Todos têm o mesmo posto (um) e comprimento (cinco).

  • Iteração: O primeiro for ciclo fornece um mecanismo para percorrer os elementos nas matrizes. O código que queres executar para calcular as somas está contido no primeiro for bloco.

  • Índice: A idx variável acede aos elementos individuais dos arrays.

Usando C++ AMP, pode escrever o seguinte código em vez disso.

#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 usados construtos AMP em C++:

  • Data: Usas arrays em C++ para construir três objetos array_view AMP em C++. Você fornece quatro valores para construir um objeto array_view: os valores dos dados, o posto, o tipo de elemento e o tamanho do objeto array_view em cada dimensão. O posto 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, o array C++ que é passado ao 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 utilizados para povoar a matriz. A biblioteca de tempo de execução inclui também a Classe do array, que tem uma interface semelhante à array_view classe e é discutida mais adiante 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 pretende executar está contido numa expressão lambda, ou função kernel. Indica restrict(amp) que apenas o subconjunto da linguagem C++ que o C++ AMP pode acelerar é utilizado.

  • Índice: A variável de classe do índice , idx, é declarada com um valor de um para corresponder ao valor do array_view objeto. Ao usar o índice, pode aceder aos elementos individuais dos array_view objetos.

Dados de Moldagem e Indexação: índice e extensão

Deve definir os valores dos dados e declarar a forma dos dados antes de poder executar o código do kernel. Todos os dados são definidos como um array (retangular), e pode definir o array para ter qualquer rank (número de dimensões). Os dados podem ter qualquer tamanho em qualquer uma das dimensões.

Classe índice

A classe de índice especifica uma localização no objeto array ou array_view, encapsulando o deslocamento a partir da origem em cada dimensão num único objeto. Quando acede a uma localização no array, passa um index objeto ao operador de indexação, [], em vez de uma lista de índices inteiros. Pode aceder aos elementos em cada dimensão usando o operador array::operator() ou o operador array_view::operator().

O exemplo seguinte cria um índice unidimensional que especifica o terceiro elemento num objeto unidimensional array_view . O índice é usado para imprimir o terceiro elemento do 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 seguinte cria um índice bidimensional que especifica o elemento onde a linha = 1 e a coluna = 2 num objeto bidimensional array_view . O primeiro parâmetro no index construtor é o componente linha, e o segundo parâmetro é o componente 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 seguinte cria um índice tridimensional que especifica o elemento onde a profundidade = 0, a linha = 1 e a coluna = 3 num objeto tridimensional array_view . Note que o primeiro parâmetro é o componente de profundidade, o segundo parâmetro é o componente de 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 . Podes criar uma extensão e usá-la para criar um array objeto OR array_view . Também pode recuperar a extensão de um objeto existente array ou array_view. O exemplo seguinte imprime o comprimento da extensão em cada dimensão de um array_view objeto.

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 seguinte cria um array_view objeto que tem as mesmas dimensões do objeto do 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";

Transferir Dados para o Acelerador: array e array_view

Dois contentores de dados usados para mover dados para o acelerador são definidos na biblioteca de tempo de execução. São a Classe Array e a Classe array_view. A array classe é uma classe contentor 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 acede aos 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 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 kernel termina, deve copiar os dados de volta para a estrutura de dados de origem. O exemplo seguinte multiplica cada elemento de um vetor por 10. Depois de a função kernel terminar, 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 kernel é executada. Portanto, se criares dois array_view objetos que usam os mesmos dados, ambos array_view referem-se ao mesmo espaço de memória. Quando fazes isto, tens de sincronizar qualquer acesso multithread. A principal vantagem de usar a array_view classe é que os dados só são movidos se forem necessários.

Comparação entre array e array_view

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

Descrição Classe array Classe array_view
Quando o posto é determinado No momento da compilação. No momento da compilação.
Quando a extensão é determinada Em tempo de execução. Em tempo de execução.
Forma Retangular. Retangular.
Armazenamento de dados É um contentor de dados. É um envoltório de dados.
Copiar Texto explícito e profundo na definição. Cópia implícita quando acedida pela função kernel.
Obtenção de dados Ao copiar os dados do array de volta para um objeto no núcleo da CPU. Por acesso direto ao array_view objeto ou chamando o método array_view::synchronize para continuar a aceder aos dados no contentor original.

Memória partilhada com array e array_view

Memória partilhada é a memória que pode ser acedida tanto pela CPU como pelo acelerador. O uso de memória partilhada elimina ou reduz significativamente a sobrecarga de cópia de dados entre a CPU e o acelerador. Embora a memória seja partilhada, não pode ser acedida simultaneamente tanto pela CPU como pelo acelerador, o que causa comportamentos indefinidos.

array Os objetos podem ser usados para especificar controlo detalhado sobre o uso de memória partilhada, se o acelerador associado o suportar. Se um acelerador suporta memória partilhada é determinado pela propriedade supports_cpu_shared_memory do acelerador, que retorna true quando a memória partilhada é suportada. Se a memória partilhada for suportada, a Enumeração do tipo de acesso predefinido para alocações de memória no acelerador é determinada pela propriedade default_cpu_access_type. Por defeito, os objetos array e array_view assumem o mesmo access_type que o accelerator principal associado.

Ao definir explicitamente a propriedade array::cpu_access_type Data Member de um array, pode exercer um controlo detalhado sobre como a memória partilhada é utilizada, para otimizar a aplicação de acordo com as características de desempenho do hardware, com base nos padrões de acesso à memória dos seus kernels de computação. Um array_view reflete o mesmo cpu_access_type que o 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 em que inicialmente solicita a alocação de armazenamento. Ou seja, se for acedido primeiro pelo anfitrião (CPU), então comporta-se como se tivesse sido criado sobre uma fonte de dados da CPU e partilha o access_type do accelerator_view associado por captura; no entanto, se for acedido primeiro por um accelerator_view, então comporta-se como se tivesse sido criado sobre um array criado nesse accelerator_view e partilha o access_type do array.

O exemplo de código seguinte mostra como determinar se o acelerador padrão suporta memória partilhada e depois cria vários arrays com 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);
}

Execução de Código sobre Dados: parallel_for_each

A função parallel_for_each define o código que pretende executar no acelerador sobre 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 parallel_for_each método utiliza dois argumentos, um domínio computacional e uma expressão lambda.

O domínio de computação é um extent objeto ou tiled_extent objeto que define o conjunto de threads a criar para execução paralela. É gerado um thread para cada elemento no domínio de computação. Neste caso, o extent objeto é unidimensional e tem cinco elementos. Por isso, iniciam-se cinco tópicos.

A expressão lambda define o código a executar em cada thread. A cláusula de captura, [=], especifica que o corpo da expressão lambda acede a todas as variáveis capturadas pelo valor, que neste caso são a, b, e sum. Neste exemplo, a lista de parâmetros cria uma variável unidimensional index chamada idx. O valor do idx[0] é 0 no primeiro fio e aumenta em um em cada fio subsequente. Indica restrict(amp) que apenas o subconjunto da linguagem C++ que o C++ AMP pode acelerar é utilizado. As limitações nas funções que têm o modificador restrict são descritas em restrict (C++ AMP). Para mais informações, veja, Sintaxe de Expressão Lambda.

A expressão lambda pode incluir o código a executar ou pode chamar uma função kernel separada. A função kernel deve incluir o restrict(amp) modificador. O exemplo seguinte é equivalente ao 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";
    }
}

Código Acelerado: Azulejos e Barreiras

Pode ganhar aceleração adicional usando tiling. A divisão em tiles divide os fios em subconjuntos retangulares iguais ou tiles. Determinas o tamanho adequado do tile com base no teu conjunto de dados e no algoritmo que estás a programar. Para cada thread, tens acesso à localização global de um elemento de dados em relação ao todo array ou array_view e acesso à localização local em relação ao bloco. Usar o valor do índice local simplifica o seu código porque não precisa de escrever o código para traduzir os valores do índice global para o local. Para usar "tiling", chame o método extent::tile no domínio de computação especificado no parallel_for_each método, e utilize um objeto tiled_index na expressão lambda.

Em aplicações típicas, os elementos num tile estão relacionados de alguma forma, e o código tem de aceder e acompanhar os valores ao longo do tile. Use a palavra-chave tile_static e o método tile_barrier::wait para conseguir isto. Uma variável que tem a palavra-chave tile_static tem um escopo em todo o tile, e uma instância dessa variável é criada para cada tile. Deve tratar da sincronização do acesso tile-thread à variável. O método tile_barrier::wait suspende a execução da thread atual até que todas as threads no tile tenham atingido o ponto de chamada para tile_barrier::wait. Assim, podes acumular valores ao longo do tile usando tile_static variáveis. Depois podes terminar quaisquer cálculos que exijam acesso a todos os valores.

O diagrama seguinte representa um array bidimensional de dados de amostragem organizados em tiles.

Valores do índice numa extensão em mosaico.

O seguinte exemplo de código utiliza os dados de amostragem do diagrama anterior. O código substitui cada valor no tile pela média dos valores no tile.

// 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 dupla precisão no espaço de nomes Concurrency::precise_math oferece suporte para funções de dupla precisão. Também oferece suporte para funções de precisão simples, embora suporte de dupla precisão no hardware ainda seja necessário. Cumpre a Especificação C99 (ISO/IEC 9899). O acelerador deve suportar precisão dupla total. Pode determinar se sim, verificando o valor do elemento de dados accelerator::supports_double_precision. A biblioteca de cálculo rápido, no espaço de nomes Concurrency::fast_math, contém um conjunto adicional de funções matemáticas. Estas funções, que suportam apenas float operandos, executam-se mais rapidamente, mas não são tão precisas como as da biblioteca de matemática de dupla precisão. 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 <ficheiro de 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> da versão C++ AMP. O código seguinte 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 Gráfica

O C++ AMP inclui uma biblioteca gráfica concebida para programação gráfica acelerada. Esta biblioteca é usada apenas em dispositivos que suportam funcionalidades gráficas nativas. Os métodos encontram-se no espaço de nomes Concurrency::graphics e estão contidos no <ficheiro de cabeçalho amp_graphics.h>. Os componentes-chave da biblioteca gráfica são:

  • Classe de textura: Podes usar a classe textura para criar texturas a partir da memória ou de um ficheiro. As texturas assemelham-se a arrays porque contêm dados, e assemelham-se a contentores na Biblioteca Padrão C++ no que diz respeito à atribuição e construção de cópias. Para mais informações, consulte Contentores de Biblioteca Padrão em C++. Os parâmetros de template para a classe texture são o tipo de elemento e a classificação. O posto pode ser 1, 2 ou 3. O tipo de elemento pode ser um dos tipos de vetores curtos que são descritos mais adiante neste artigo.

  • writeonly_texture_view Class: Fornece acesso apenas de escrita 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, norma ou unorm.

Aplicações da Plataforma Universal Windows (UWP)

Tal como noutras bibliotecas C++, podes usar C++ AMP nas tuas aplicações UWP. Estes artigos descrevem como incluir código AMP em C++ em aplicações criadas usando C++, C#, Visual Basic ou JavaScript:

AMP C++ e Visualizador de Concorrência

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

Recomendações de Desempenho

O módulo e a divisão de inteiros sem sinal têm desempenho significativamente melhor do que o módulo e divisão de inteiros com sinal. Recomendamos que utilize inteiros sem sinal sempre que possível.

Consulte também

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