Passo a passo: Multiplicação de matriz
Esta explicação passo a passo demonstra como usar o C++ AMP para acelerar a execução de uma multiplicação de matriz.Dois algoritmos são apresentados, um sem disposição lado a lado, e um com disposição lado a lado.
Pré-requisitos
Antes de iniciar:
Leia Visão geral do C++ AMP.
Leia Usando imagens.
Certifique-se de que Windows 7, Windows 8, Windows Server 2008 R2, ou Windows Server 2012 está instalado no seu computador.
Para criar o projeto
Na barra de menus do Visual Studio, escolha Arquivo, Novo, Projeto.
Em baixo de Instalados no painel de modelos, selecione Visual C++.
Selecione Projeto Vazio, digite MatrixMultiply na caixa de Nome, e escolha o botão OK.
Escolha o botão Próximo.
No Explorador de Soluçẽs, abra o menu de atalho para Arquivos Fontes, e escolha Adicionar, Novo Item.
Na caixa de diálogo Adicionar Novo Item selecione Arquivo C++ (. cpp), digite MatrixMultiply.cpp na caixa Nome e escolha o botão Adicionar.
Multiplicação sem disposição lado a lado
Nesta seção, considere a multiplicação de duas matrizes, A e B, que estão definidas como segue:
A é uma matriz 3 por 2 e B é uma matriz 2 por 3.O produto da multiplicação de A por B é a seguinte matriz 3 por 3.O produto é calculado multiplicando-se as linhas de A pelas colunas de B elemento por elemento.
Para multiplicar sem usar C++ AMP
Abra MatrixMultiply.cpp e use o seguinte código para substituir o código existente.
#include <iostream> void MultiplyWithOutAMP() { int aMatrix[3][2] = {{1, 4}, {2, 5}, {3, 6}}; int bMatrix[2][3] = {{7, 8, 9}, {10, 11, 12}}; int product[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { // Multiply the row of A by the column of B to get the row, column of product. for (int inner = 0; inner < 2; inner++) { product[row][col] += aMatrix[row][inner] * bMatrix[inner][col]; } std::cout << product[row][col] << " "; } std::cout << "\n"; } } void main() { MultiplyWithOutAMP(); getchar(); }
O algoritmo é uma implementação simples da definição de multiplicação de matriz.Ele não usa nenhum algoritmo paralelo ou encadeado para reduzir o tempo de computação.
Na barra de menu, escolha Arquivo, Salvar Tudo.
Pressione o atalho de teclado F5 para iniciar depuração e verifique que a saída está correta.
Pressione Enter para sair do aplicativo.
Para multiplicar usando C++ AMP
No MatrixMultiply.cpp, adicione o seguinte código antes do método main.
void MultiplyWithAMP() { int aMatrix[] = { 1, 4, 2, 5, 3, 6 }; int bMatrix[] = { 7, 8, 9, 10, 11, 12 }; int productMatrix[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; array_view<int, 2> a(3, 2, aMatrix); array_view<int, 2> b(2, 3, bMatrix); array_view<int, 2> product(3, 3, productMatrix); parallel_for_each( product.extent, [=](index<2> idx) restrict(amp) { int row = idx[0]; int col = idx[1]; for (int inner = 0; inner < 2; inner++) { product[idx] += a(row, inner) * b(inner, col); } } ); product.synchronize(); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { //std::cout << productMatrix[row*3 + col] << " "; std::cout << product(row, col) << " "; } std::cout << "\n"; } }
O código AMP assemelha-se ao código não-AMP.A chamada para o parallel_for_each inicia um thread para cada elemento no product.extente substitui o laço for por linha e coluna.O valor da célula na linha e coluna está disponível em idx.Você pode acessar os elementos de um objeto array_view usando o operador [] e uma variável de índice, ou o operador () e as variáveis de linha e coluna.O exemplo demonstra ambos os métodos.O método array_view::synchronize copia os valores da variável product de volta para a variável productMatrix.
Adicione as instruções seguintes na parte superior do MatrixMultiply.cpp: include e using.
#include <amp.h> using namespace concurrency;
Modifique o método main para chamar o método MultiplyWithAMP.
void main() { MultiplyWithOutAMP(); MultiplyWithAMP(); getchar(); }
Pressione o atalho de teclado Ctrl + F5 para iniciar a depuração e verifique que a saída está correta.
Pressione a barra de espaços para sair do aplicativo.
Multiplicação com disposição lado a lado
Disposição Lado a lado (tiling) é uma técnica na qual você particiona dados em subconjuntos de tamanho igual, que são conhecidos como tiles.Três coisas mudam quando você usa a disposição lado a lado.
Você pode criar variáveis tile_static.Acesso a dados no espaço tile_static pode ser muitas vezes mais rápido do que o acesso a dados no espaço global.Uma instância de uma variável tile_static é criada para cada tile, e todos os threads no tile tem acesso à variável.O principal benefício da disposição lado a lado é o ganho de desempenho devido ao acesso tile_static.
Você pode chamar o método tile_barrier::wait para interromper todos os threads em um tile em uma linha específiva do código.Você não pode garantir a ordem em que os threads serão executados, só que todos os threads em um tile irão parar na chamada para o tile_barrier::wait antes de continuar a execução.
Você tem acesso ao índice do thread relativo a todo o objeto array_view e o índice relativo ao tile.Usando o índice local, você pode tornar seu código mais fácil de ler e depurar.
Para tirar vantagem da disposição lado a lado na multiplicação de matriz, o algoritmo deve particionar a matriz em tiles e copiar os dados tile nas variáveis tile_static para acesso mais rápido.Neste exemplo, a matriz é particionada em submatrizes de tamanho igual.O produto é encontrado multiplicando as submatrizes.As duas matrizes e seus produtos neste exemplo são:
As matrizes são particionadas em quatro matrizes 2x2, que são definidas da seguinte maneira:
O produto de A e B agora pode ser escrito e calculado da seguinte forma:
Porque as matrizes a a h são matrizes 2x2, todos os produtos e somas delas também são matrizes 2x2.Também segue que A*B é uma matriz 4 x 4, conforme o esperado.Para verificar rapidamente o algoritmo, calcule o valor do elemento da primeira linha e primeira coluna no produto.No exemplo, aquele seria o valor do elemento da primeira linha e primeira coluna de ae + bg.Você só precisa calcular a primeira coluna, a primeira linha de ae e bg para cada termo.O valor para ae é 1*1 + 2*5 = 11.O valor para bg é 3*1 + 4*5 = 23.O valor final é 11 + 23 = 34, que está correto.
Para implementar esse algoritmo, o código:
Usa um objeto tiled_extent em vez de um objeto extent na chamada parallel_for_each.
Usa um objeto tiled_index em vez de um objeto index na chamada parallel_for_each.
Cria variáveis tile_static para armazenar as submatrizes.
Usa o método tile_barrier::wait para parar os threads para o cálculo dos produtos das submatrizes.
Para multiplicar usando AMP e disposição lado a lado
No MatrixMultiply.cpp, adicione o seguinte código antes do método main.
void MultiplyWithTiling() { // The tile size is 2. static const int TS = 2; // The raw data. int aMatrix[] = { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 }; int bMatrix[] = { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 }; int productMatrix[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // Create the array_view objects. array_view<int, 2> a(4, 4, aMatrix); array_view<int, 2> b(4, 4, bMatrix); array_view<int, 2> product(4, 4, productMatrix); // Call parallel_for_each by using 2x2 tiles. parallel_for_each(product.extent.tile< TS, TS >(), [=] (tiled_index< TS, TS> t_idx) restrict(amp) { // Get the location of the thread relative to the tile (row, col) and the entire array_view (rowGlobal, colGlobal). int row = t_idx.local[0]; int col = t_idx.local[1]; int rowGlobal = t_idx.global[0]; int colGlobal = t_idx.global[1]; int sum = 0; // Given a 4x4 matrix and a 2x2 tile size, this loop executes twice for each thread. // For the first tile and the first loop, it copies a into locA and e into locB. // For the first tile and the second loop, it copies b into locA and g into locB. for (int i = 0; i < 4; i += TS) { tile_static int locA[TS][TS]; tile_static int locB[TS][TS]; locA[row][col] = a(rowGlobal, col + i); locB[row][col] = b(row + i, colGlobal); // The threads in the tile all wait here until locA and locB are filled. t_idx.barrier.wait(); // Return the product for the thread. The sum is retained across // both iterations of the loop, in effect adding the two products // together, for example, a*e. for (int k = 0; k < TS; k++) { sum += locA[row][k] * locB[k][col]; } // All threads must wait until the sums are calculated. If any threads // moved ahead, the values in locA and locB would change. t_idx.barrier.wait(); // Now go on to the next iteration of the loop. } // After both iterations of the loop, copy the sum to the product variable by using the global location. product[t_idx.global] = sum; }); // Copy the contents of product back to the productMatrix variable. product.synchronize(); for (int row = 0; row < 4; row++) { for (int col = 0; col < 4; col++) { // The results are available from both the product and productMatrix variables. //std::cout << productMatrix[row*3 + col] << " "; std::cout << product(row, col) << " "; } std::cout << "\n"; } }
Este exemplo é significativamente diferente do exemplo sem a disposição lado a lado.O código usa estas etapas conceituais:
Copiar os elementos do tile [0,0] do a em locA.Copiar os elementos do tile [0,0] do b em locB.Observe que product está disposto lado a lado, e não a e b.Portanto, você usa índices globais para acessar a, b, e product.A chamada para tile_barrier::wait é essencial.Ele bloqueia todos os threads no tile até ambos locA e locB estarem preenchidos.
Multiplicar locA e locB e colocar os resultados em product.
Copiar os elementos do tile [0,1] do a em locA.Copiar os elementos do tile [1,0] do b em locB.
Multiplicar locA e locB e adicioná-los para os resultados que já estão em product.
A multiplicação de tile [0,0] está completa.
Repita para os outros quatro tiles.Não há nenhuma indexação específica para os tiles e os threads podem executar em qualquer ordem.A medida que cada thread executa, as variáveis tile_static são criadas para cada tile apropriadamente e a chamada para tile_barrier::wait controla o fluxo do programa.
A medida que você examina atentamente o algoritmo, observe que cada submatriz é carregada em uma memória tile_static duas vezes.Esta transferência de dados leva tempo.No entanto, uma vez que o dado está na memória tile_static, o acesso ao dado é muito mais rápido.Como calcular os produtos requer acesso repetido para valores nas submatrizes, há um ganho de desempenho geral.Para cada algoritmo, é necessário experimentação para localizar o algoritmo e tamanho do tile mais adequados.
Nos exemplos de não-AMP e não-tile, cada elemento de A e B é acessado quatro vezes da memória global para calcular o produto.No exemplo do tile, cada elemento é acessado duas vezes da memória global e quatro vezes da memória tile_static.Isso não é um ganho de desempenho significativo.No entanto, se A e B fossem matrizes 1024x1024 e o tamanho do tile fosse 16, o ganho de desempenho seria significativo.Nesse caso, cada elemento seria copiado para a memória tile_static somente 16 vezes e acessados a partir da memória tile_static 1024 vezes.
Modifique o método principal para chamar o método MultiplyWithTiling , como mostrado.
void main() { MultiplyWithOutAMP(); MultiplyWithAMP(); MultiplyWithTiling(); getchar(); }
Pressione o atalho de teclado Ctrl + F5 para iniciar a depuração e verifique que a saída está correta.
Pressione a barra de espaço para sair do aplicativo.
Consulte também
Tarefas
Passo a passo: Depuração de um aplicativo de AMP C++