Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
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
forciclo fornece um mecanismo para percorrer os elementos nas matrizes. O código que queres executar para calcular as somas está contido no primeiroforbloco.Índice: A
idxvariá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 objetoarray_viewem 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 objetoarray_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_viewclasse 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. Indicarestrict(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 doarray_viewobjeto. Ao usar o índice, pode aceder aos elementos individuais dosarray_viewobjetos.
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.
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
texturesã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:
Guia: Criar um componente básico de Runtime do Windows em C++ e chamá-lo a partir do JavaScript
Bing Maps Trip Optimizer, uma aplicação da Windows Store em JavaScript e C++
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