Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Uma introdução com base em código ao C++ AMP
Este artigo aborda uma tecnologia em pré-lançamento, chamada C++ AMP, que será fornecida com o Visual Studio 11. Todas as informações estão sujeitas a alterações.
O Visual Studio 11 oferece suporte à computação, da heterogênea à tradicional, através de uma tecnologia chamada C++ Accelerated Massive Parallelism (C++ AMP). Isso permite que você aproveite as vantagens de aceleradores, como GPUs, para acelerar algoritmos paralelos de dados.
O C++ AMP fornece desempenho, com portabilidade entre hardware, sem comprometer a produtividade à qual você se acostumou a esperar do moderno C++ e do pacote do Visual Studio. Ele é capaz de oferecer ganhos de velocidade, em ordens de grandeza, quando comparada ao uso só do CPU. Em conferências, eu em geral demonstro um único processo que aproveita os GPUs NVIDIA e AMD, enquanto continuo contando com uma solução fallback baseada em CPU.
Nesta introdução controlada por código que explora o C++ AMP, vou supor que você está lendo cada linha de código deste artigo. O código embutido é uma parte fundamental do artigo e o que se encontra em código C++ não será necessariamente repetido no texto do artigo.
Algoritmo de configuração e exemplo
Primeiro, vamos entender o simples algoritmo com o qual estaremos trabalhando, juntamente com o necessário código de configuração, preparando-nos para, mais tarde, convertermos para o uso do C++ AMP.
Crie um projeto C++ vazio, adicione um novo arquivo C++ vazio (Source.cpp) e digite o seguinte código autoexplicativo (estou usando números de linha não contíguos para facilitar a explicação no texto do artigo, e você irá encontrar os mesmos números de linha no projeto fornecido para download):
1 #include <amp.h> // C++ AMP header file
3 #include <iostream> // For std::cout etc
4 using namespace concurrency; // Save some typing :)
5 using std::vector; // Ditto. Comes from <vector> brought in by amp.h
6
79 int main()
80 {
81 do_it();
82
83 std::cout << "Hit any key to exit..." << std::endl;
84 std::cin.get();
85 }
O C++ AMP introduz uma série de tipos em diversos arquivos de cabeçalho. De acordo com as linhas 1 e 4 do trecho de código anterior, o principal arquivo de cabeçalho é amp.h e os principais tipos são adicionados ao namespace de simultaneidade existente. Não é necessária nenhuma configuração ou opção de compilação adicional para se usar o C++ AMP. Vamos agora adicionar uma função do_it acima do principal (consulte a Figura 1).
Figura 1 A função do_it, chamada a partir do principal
52 void do_it()
53 {
54 // Rows and columns for matrix
55 const int M = 1024;
56 const int N = 1024;
57
58 // Create storage for a matrix of above size
59 vector<int> vA(M * N);
60 vector<int> vB(M * N);
61
62 // Populate matrix objects
63 int i = 0;
64 std::generate(vA.begin(), vA.end(), [&i](){return i++;});
65 std::generate(vB.begin(), vB.end(), [&i](){return i--;});
66
67 // Output storage for matrix calculation
68 vector<int> vC(M * N);
69
70 perform_calculation(vA, vB, vC, M, N);
76 }
Nas linhas 59, 60 e 68, o código usa objetos std::vector como contêineres simples para cada matriz, mesmo que um tipo bidimensional seja aquele com o qual você gostaria de estar lidando. Abordaremos esse assunto mais tarde.
É importante que se entenda o uso de expressões lambda nas linhas 64 e 65, que são passadas ao método std::generate para popular os dois objetos vetoriais. Esse artigo supõe que você saiba usar, com proficiência, lambdas em C++. Por exemplo, você deverá entender imediatamente que se a variável i for capturada por valor (modificando-se a lista de captura assim [i] ou assim [=] e usando-se a palavra-chave mutável) então cada membro do vetor deverá ser inicializado como 0! Se você não se sente confortável a usar lambdas (uma maravilhosa adição ao padrão do C++ 11), leia o artigo da Biblioteca MSDN sobre expressões lambda em C++ (msdn.microsoft.com/library/dd293608) e retorne quando estiver preparado.
A função do_it introduziu uma chamada a perform_calculation, que está codificada abaixo:
7 void perform_calculation(
8 vector<int>& vA, vector<int>& vB, vector<int>& vC, int M, int N)
9 {
15 for (int i = 0; i < M; i++)
16 {
17 for (int j = 0; j < N; j++)
18 {
19 vC[i * N + j] = vA[i * N + j] + vB[i * N + j];
20 }
22 }
24 }
Nesse exemplo bem simples de adição de matrizes, algo que chama a atenção é que a multidimensionalidade da matriz é perdida devido ao armazenamento linearizado da matriz em um objeto vetorial (o que explica porque você precisou passar as dimensões da matriz juntamente com os objetos vetoriais). Além disso, é preciso praticar uma aritmética estranha com os índices da linha 19. Esse ponto teria sido ainda mais óbvio se você desejasse adicionar submatrizes dessas matrizes.
Até agora, nenhum código C++ AMP foi feito. Em seguida, alterando a função perform_calculation, você verá como pode começar a introduzir alguns tipos C++ AMP. Em seções mais adiante, você aprenderá como tirar total vantagem do C++ AMP e acelerar seus algoritmos paralelos de dados.
array_view<T, N>, extent<N> e index<N>
O C++ AMP introduz um tipo concurrency::array_view para envolver contêineres de dados. Você pode considerá-lo como um ponteiro inteligente. Ele representa dados em forma retangular, contíguos pela dimensão menos significativa. Mais tarde, a razão de sua existência irá se tornar óbvia e você verá alguns aspectos de seu uso. Vamos alterar o corpo da função perform_calculation da seguinte maneira:
11 array_view<int> a(M*N, vA), b(M*N, vB);
12 array_view<int> c(M*N, vC);
14
15 for (int i = 0; i < M; i++)
16 {
17 for (int j = 0; j < N; j++)
18 {
19 c(i * N + j) = a(i * N + j) + b(i * N + j);
20 }
22 }
Essa função, que compila e executa na CPU, tem a mesma saída que a anterior. A única diferença é o uso gratuito dos objetos array_view que são introduzidos nas linhas 11 e 12. A linha 19 tem ainda a indexação estranha (por enquanto), mas agora está usando os objetos array_view (a, b, c) em vez dos objetos vetoriais (vA, vB e vC) e está acessando os elementos através do operador da função array_view (em contraste com a utilização anterior do operador subscrito vetorial — mais informações adiante).
Será necessário informar ao array_view, por meio de um argumento de modelo (int, nesse exemplo) o tipo de elemento do contêiner que ele envolve; você irá transmitir o contêiner como o último argumento construtor (por exemplo, a variável VC do tipo vetorial na linha 12). O primeiro argumento construtor é o número de elementos.
É possível, também, especificar o número de elementos com um objeto concurrency::extent de modo que você possa alterar as linhas 11 e 12 da seguinte maneira:
10 extent<1> e(M*N);
11 array_view<int, 1> a(e, vA), b(e, vB);
12 array_view<int, 1> c(e, vC);
O objeto extent<N> representa um espaço multidimensional, onde a classificação é passada como um argumento de modelo. O argumento de modelo é 1 nesse exemplo, mas a classificação pode ser qualquer valor maior que zero. O construtor de extensão aceita o tamanho de cada dimensão que o objeto de extensão representa, como mostra a linha 10. O objeto de extensão pode ser passado ao construtor de objeto array_view para definir sua forma, como mostrado nas linhas 11 e 12. Naquelas linhas, eu também acrescentei um segundo argumento de modelo ao array_view, indicando que ele representa um espaço unidimensional — como no exemplo de código anterior, eu poderia, com segurança, ter omitido isso, uma vez que 1 é a classificação padrão.
Agora que você tem informação sobre esses tipos, poderá fazer modificações adicionais à função, de modo que ela possa acessar os dados de uma maneira bidimensional mais natural, o que lembra mais de pero a realidade das matrizes.
10 extent<2> e(M, N);
11 array_view<int, 2> a(e, vA), b(e, vB);
12 array_view<int, 2> c(e, vC);
14
15 for (int i = 0; i < e[0]; i++)
16 {
17 for (int j = 0; j < e[1]; j++)
18 {
19 c(i, j) = a(i, j) + b(i, j);
20 }
22 }
As alterações nas linhas de 10 a 12 tornam os objetos array_view bidimensionais, de modo que serão necessários dois índices para acessar um elemento. As linhas 15 e 17 acessam os limites da extensão por meio de seu operador subscrito em vez de usar diretamente as variáveis M e N; uma vez que a forma houver sido encapsulada na extensão, você poderá, então, usar o objeto por toda a extensão de seu código.
A alteração importante está na linha 19, onde não há mais necessidade de se usar aritmética estranha. A indexação é muito mais natural, tornando o próprio algoritmo completo muito mais legível e de fácil manutenção.
Se o array_view for criado com uma extensão tridimensional, então o operador de função deve esperar por uma extensão tridimensional para acessar um elemento, ainda a partir da dimensão mais significativa para a menos significativa. Como se deve esperar de uma API multidimensional, há também uma maneira de indexar em um array_view por meio de um objeto único passado a seu operador subscrito. O objeto deve ser do tipo concurrency::index<N>, onde N corresponde à classificação da extensão com a qual o array_view é criado. Você verá mais tarde como objetos de índice podem ser passados para o seu código, mas, por enquanto, vamos criar um manualmente para ter uma ideia de como são e vê-los em ação, modificando o corpo da função da seguinte maneira:
10 extent<2> e(M, N);
11 array_view<int, 2> a(e, vA), b(e, vB);
12 array_view<int, 2> c(e, vC);
13
14 index<2> idx(0, 0);
15 for (idx[0] = 0; idx[0] < e[0]; idx[0]++)
16 {
17 for (idx[1] = 0; idx[1] < e[1]; idx[1]++)
18 {
19 c[idx] = a[idx] + b[idx];
//19 //c(idx[0], idx[1]) = a(idx[0], idx[1]) + b(idx[0], idx[1]);
20 }
22 }
Como se pode ver nas linhas 14, 15, 17 e 19, o tipo concurrency::index<N> tem uma interface muito similar à do tipo de extensão, exceto pelo índice que representa um ponto N-dimensional, em vez de um espaço N-dimensional. Tanto a extensão como os tipos de índice suportam uma série de operações aritméticas por meio de sobrecarregamento de operadores — por exemplo, a operação de incremento no exemplo anterior.
Anteriormente, as variáveis de loop (i e j) eram usadas para indexar no array_view e agora elas podem ser substituídas por um único objeto de índice, na linha 19. Isso demonstra como, usando o operador subscrito array_view, você consegue indexar nele com uma única variável (nesse exemplo, idx do tipo index<2>).
Agora, você tem um entendimento básico dos três novos tipos introduzido com o C++ AMP: array_view<T,N>, extent<N> e index<N> Eles têm mais a oferecer, como mostrado nos diagramas de classe na Figura 2.
Figura 2 Classes array_view, extent e index
A verdadeira força e motivação por trás do uso dessa API multidimensional estão na execução de algoritmos em um acelerador paralelo de dados, como o GPU. Para fazer isso, você precisa de um ponto de entrada na API para execução de seu código no acelerador, mais uma maneira de verificar, em tempo de compilação, se você está usando um subconjunto da linguagem C++ que possa ser executado com eficiência em tal acelerador.
parallel_for_each e restrict(amp)
A API que instrui o tempo de execução do C++ AMP a receber sua função e executá-la no acelerador é uma nova sobrecarga para concurrency::parallel_for_each. Ela aceita dois argumentos: um objeto de extensão e uma lambda.
O objeto extent<N>, com o qual você já está familiarizado, é usado para determinar quantas vezes a lambda irá ser chamada pelo acelerador e você deve presumir que, a cada vez, haverá um thread separado chamando seu código, com potencial simultaneidade, sem nenhuma garantia de sequência. Por exemplo uma extent<1>(5) irá resultar em cinco chamadas à lambda que você passar ao parallel_for_each, enquanto que uma extent<2>(3,4) resultará em 12 chamadas à lambda. Em algoritmos reais, você estará, geralmente, agendando milhares de chamadas para a sua lambda.
A lambda deve aceitar um objeto index<N>, com o qual você já está familiarizado. O objeto de índice deve ter a mesma classificação que o objeto de extensão passado a parallel_for_each. É claro que o valor de índice será diferente cada vez que sua lambda for chamada — é assim que você poderá distinguir entre duas invocações diferentes para sua lambda. Você pode considerar o valor de índice como a ID do thread.
Segue uma representação em código do que eu descrevi até agora, com parallel_for_each:
89 extent<2> e(3, 2);
90 parallel_for_each(e,
91 [=](index<2> idx)
92 {
93 // Code that executes on the accelerator.
94 // It gets invoked in parallel by multiple threads
95 // once for each index "contained" in extent e
96 // and the index is passed in via idx.
97 // The following always hold true
98 // e.rank == idx.rank
99 // e.contains(idx) == true
100 // the function gets called e.size() times
101 // For this two-dimensional case (.rank == 2)
102 // e.size() == 3*2 = 6 threads calling this lambda
103 // The 6 values of idx passed to the lambda are:
104 // { 0,0 } { 0,1 } { 1,0 } { 1,1 } { 2,0 } { 2,1 }
105 }
106 );
107 // Code that executes on the host CPU (like line 91 and earlier)
O código simples, sem uma importante adição à linha 91, não será compilado:
error C3577: Concurrency::details::_Parallel_for_each argument #3 is illegal: missing public member: 'void operator()(Concurrency::index<_Rank>) restrict(amp)'
Da maneira como o código foi escrito, não haveria nada que o impedisse de usar no corpo da lambda (linhas 92 - 105) qualquer coisa permitida pela linguagem completa C++ (conforme suportada pelo compilador Visual C++). Entretanto, há restrições quanto ao uso de certos aspectos da linguagem C++ nas atuais arquiteturas GPU, de modo que você deve indicar as partes de seu código que devem cumprir essas restrições (para que você possa descobrir, em tempo de compilação, se está infringindo alguma regra). A indicação deve ser feita na lambda e em quaisquer outras assinaturas de função chamadas a partir de sua lambda. Desse modo, você deverá modificar a linha 91 da seguinte maneira:
91 [=](index<2> idx) restrict(amp)
Esse é um recurso novo e importante da especificação do C++ AMP que foi acrescentado ao compilador do Visual C++. Funções (inclusive lambdas) podem ser anotadas com restrict(cpu), que é o padrão implícito, ou com restrict(amp), como mostrado no exemplo de código anterior, ou com uma combinação de ambos — por exemplo, restrict(cpu, amp). Não há outras opções. A anotação torna-se parte da assinatura da função e assim participa da sobrecarga, que foi a principal motivação para sua criação. Quando uma função é anotada com restrict(amp), é verificada quanto a uma série de restrições e, se alguma delas for violada, você receberá um erro do compilador. O conjunto completo de restrições encontra-se documentado na postagem de blog abaixo: bit.ly/vowVlV.
Uma das restrições de restrict(amp) para lambdas é que elas não são capazes de capturar variáveis por referência (veja a advertência ao final deste artigo), nem são capazes de capturar ponteiros. Com essa restrição em mente, ao olhar para a última listagem de código para parallel_for_each, você estaria correto ao imaginar: "se não posso capturar por referência e não posso capturar ponteiros, como é que vou observar os resultados — isto é, os efeitos colaterais desejáveis — a partir da lambda? Quaisquer alterações que eu faça nas variáveis que eu capturar por valor não estarão disponíveis para o código externo assim que a lambda for concluída."
A resposta a essa dúvida é um tipo que você já conhece: array_view. O objeto array_view pode ser capturado por valor na lambda. Ele é seu mecanismo para passar dados para dentro e para fora. Simplesmente use objetos array_view para encapsular contêineres reais. Em seguida, capture os objetos array_view na lambda para acesso e preenchimento e, então, acesse os objetos array_view adequados após a chamada para parallel_for_each.
Juntando tudo isso
Com esse novo conhecimento, você pode agora examinar a adição de matrizes anterior, em CPU serial (aquela que usou array_view, extensão e índice) e substituir as linhas de 15 a 22 da seguinte maneira:
15 parallel_for_each(e, [=](index<2> idx) restrict(amp)
16 {
19 c[idx] = a[idx] + b[idx];
22 });
Observe que a linha 19 permaneceu a mesma e que o loop duplamente aninhado com criação manual de objeto de índice dentro dos limites da extensão foi substituído por uma chamada à função parallel_for_each.
Ao trabalhar com aceleradores discretos que possuem sua própria memória, a captura dos objetos array_view na lambda é passada para resultados parallel_for_each em uma cópia dos dados subjacentes à memória global do acelerador. De maneira similar, após a chamada parallel_for_each, quando você acessar os dados via o objeto array_view (nesse exemplo, via c) os dados serão copiados do acelerador na memória do host.
Saiba que se você desejar acessar os resultados de array_view c via o contêiner original vC (e não via o array_view), então deverá chamar o método synchronize do objeto array_view. O código funcionaria da maneira que se encontra agora, porque o destruidor array_view chama synchronize em seu nome, mas quaisquer exceções se perderiam dessa maneira, de modo que recomendo que, em vez disso, você chame synchronize explicitamente. Assim, acrescente uma instrução em qualquer local depois da chamada do parallel_for_each, desta maneira:
23 c.synchronize();
O inverso (garantindo que o array_view tenha os dados mais atuais de seu contêiner original, se eles mudarem) é conseguido via o método refresh.
E mais importante ainda, copiar dados em (geralmente) um barramento PCIe pode ser muito caro, de modo que é recomendável copiar dados somente na direção necessária. Na lista anterior, você pode modificar as linhas de 11 a 13 para indicar que os dados subjacentes dos objetos array_view a e b devem ser copiados no acelerador (mas não serão copiados de volta) e, também, que os dados subjacentes de array_view c não precisam ser copiados no acelerador. As alterações necessárias aparecem em negrito no trecho abaixo:
11 array_view<const int, 2> a(e, vA), b(e, vB);
12 array_view<int, 2> c(e, vC);
13 c.discard_data();
Entretanto, mesmo com essas modificações em vigor, o algoritmo de adição de matrizes não faz suficiente uso intenso de computador para compensar a sobrecarga para copiar dados, de modo que não é um bom candidato à paralelização com o C++ AMP. Eu o tenho usado somente para ensinar os fundamentos.
Dito isso, usando esse simples exemplo por todo o artigo, você está apto agora a paralelizar outros algoritmos que fazem uso intensivo e suficiente de computador para produzir benefícios. Um desses algoritmos é a multiplicação de matrizes. Sem nenhum comentário de minha parte, garanta que você entendeu essa simples implementação serial do algoritmo de multiplicação de matrizes:
void MatMul(vector<int>& vC, const vector<int>& vA,
const vector<int>& vB, int M, int N, int W)
{
for (int row = 0; row < M; row++)
{
for (int col = 0; col < N; col++)
{
int sum = 0;
for(int i = 0; i < W; i++)
sum += vA[row * W + i] * vB[i * N + col];
vC[row * N + col] = sum;
}
}
}
... e a implementação C++ AMP correspondente:
array_view<const int, 2> a(M, W, vA), b(W, N, vB);
array_view<int, 2> c(M, N, vC);
c.discard_data();
parallel_for_each(c.extent, [=](index<2> idx) restrict(amp)
{
int row = idx[0]; int col = idx[1];
int sum = 0;
for(int i = 0; i < b.extent[0]; i++)
sum += a(row, i) * b(i, col);
c[idx] = sum;
});
c.synchronize();
Em meu laptop, a multiplicação de matrizes com o C++ AMP rende um aumento de desempenho 40 vezes maior quando comparada ao código de CPU serial para M=N=W=1024.
Agora que você compreende os fundamentos, deve estar imaginando como escolher um acelerador no qual irá executar seu algoritmo, uma vez que o tenha implementado usando o C++ AMP. Vamos examinar isso em seguida.
accelerator e accelerator_view
Parte do namespace de simultaneidade é o novo tipo de acelerador. Ele representa um dispositivo no sistema, que o tempo de execução do C++ AMP pode usar, o qual, na versão inicial é hardware com um driver DirectX 11 instalado (ou emuladores de DirectX).
Quando o tempo de execução do C++ AMP inicia, ele enumera todos os aceleradores e, com base em heurística interna, escolhe um como o padrão. É por isso que você não precisou lidar diretamente com aceleradores em todo o código anterior — um padrão foi escolhido para você. Se você deseja enumerar os aceleradores e até mesmo escolher o padrão por sua conta, isso é muito fácil de fazer, como mostra o código autoexplicativo da Figura 3.
Figura 3 Escolhendo um acelerador
26 accelerator pick_accelerator()
27 {
28 // Get all accelerators known to the C++ AMP runtime
29 vector<accelerator> accs = accelerator::get_all();
30
31 // Empty ctor returns the one picked by the runtime by default
32 accelerator chosen_one;
33
34 // Choose one; one that isn't emulated, for example
35 auto result =
36 std::find_if(accs.begin(), accs.end(), [] (accelerator acc)
37 {
38 return !acc.is_emulated; //.supports_double_precision
39 });
40 if (result != accs.end())
41 chosen_one = *(result); // else not shown
42
43 // Output its description (tip: explore the other properties)
44 std::wcout << chosen_one.description << std::endl;
45
46 // Set it as default ... can only call this once per process
47 accelerator::set_default(chosen_one.device_path);
48
49 // ... or just return it
50 return chosen_one;
51 }
Na linha 38, você pode observar a consulta de uma das muitas propriedades de acelerador e outras são mostradas na Figura 4.
Figura 4 Classes accelerator e accelerator_view
Se você deseja ter chamadas parallel_for_each diferentes que usem diferentes aceleradores, ou se, por qualquer razão que seja, deseja ser mais explícito do que definir o acelerador padrão globalmente para um processo, então você terá que passar um objeto accelerator_view ao parallel_for_each. Isso é possível porque parallel_for_each tem uma sobrecarga que aceita um accelerator_view como o primeiro parâmetro. Obter um objeto accelerator_view é tão fácil quanto chamar default_view em um objeto acelerador, por exemplo:
accelerator_view acc_vw = pick_accelerator().default_view;
Além do hardware DirectX 11 existem três aceleradores especiais disponibilizados pelo C++ AMP:
- direct3d_ref: útil para depuração de correção, mas sem utilidade para produção, pois é extremamente mais lento que qualquer hardware real.
- direct3d_warp: uma solução fallback para execução de código C++ AMP no CPU usando vários núcleos e, hoje, Extensões SIMD de Streaming.
- cpu_accelerator: totalmente incapaz de executar C++ AMP, nessa versão. Só é útil para configuração de matrizes de preparação (um técnica de otimização avançada), o que vai além do escopo deste artigo mas está descrito na seguinte postagem de blog: bit.ly/vRksnn.
Agrupamento lado a lado e leitura adicional por sua conta
O tópico mais importante que não foi abordado neste artigo é o agrupamento lado a lado.
De uma perspectiva de cenário, agrupamento lado a lado usa os ganhos de desempenho em ordens de grandeza que você observa com as técnicas de codificação exploradas até agora e (potencialmente) torna os ganhos ainda maiores. De uma perspectiva de API, agrupamento lado a lado consiste em tipos tiled_index e tiled_extent, assim como um tipo tile_barrier e uma classe de armazenamento tile_static. Há também uma sobrecarga ao parallel_for_each que aceita um objeto tiled_extent e cuja lambda aceita um objeto tiled_index. Nessa lambda, vai ser permitido usar objetos tile_barrier e variáveis tile_static. Eu abordo agrupamento lado a lado no meu segundo artigo sobre C++ AMP, na página 40.
Existem outros tópicos que você pode explorar por sua conta com a ajuda de postagens de blogs e da documentação online do MSDN:
- <amp_math.h> é uma biblioteca matemática com dois namespaces, um para funções matemáticas de alta precisão e outro para funções matemáticas rápidas, mas menos precisas. Inscreva-se, baseado em seus recursos de hardware e seus requisitos de cenário.
- <amp_graphics.h>, <amp_short_vectors.h> e mais algumas funções interoperacionais DirectX encontram-se disponíveis para trabalhar com programação de gráficos.
- concurrency::array é um tipo de dados de contêiner que se liga a um acelerador, com uma interface quase que idêntica ao array_view. Esse tipo é um dos dois tipos (sendo o outro, textura no namespace de gráficos) que precisa ser capturado por referência na lambda passada a parallel_for_each. Essa é a limitação à qual me referi anteriormente no artigo.
- Suporte a aspectos intrínsecos do DirectX como atômicos para sincronização entre threads.
- Depuração em GPU e criação de perfil no Visual Studio 11.
Proteção futura de seus investimentos
Neste artigo, introduzi você a uma API moderna de dados paralela em C++ que permite expressar seus algoritmos de modo a permitir que seu aplicativo utilize GPUs para obter ganhos de desempenho. O C++ AMP é projetado para proteção futura de seus investimentos em relação a hardware ainda não existente.
Você aprendeu agora como uma quantidade de tipos (array_view, extent e index) podem ajudá-lo com dados multidimensionais, combinados a uma única função global (parallel_for_each) que permite que você execute seu código — começando a partir de uma lambda restrict(amp) — em um acelerador (que você pode especificar via objetos accelerator e accelerator_view).
Além da implementação do Microsoft Visual C++, o C++ AMP é disponibilizado à comunidade como uma especificação aberta que qualquer um pode implementar em qualquer plataforma.
Daniel Moth é gerente de programas chefe na Divisão de desenvolvedores da Microsoft. Para contatá-lo, visite seu blog em danielmoth.com/Blog.
Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Steve Deitz, Yossi Levanoni, Robin Reynolds-Haertle, Stephen Toub e Weirong Zhu