Partilhar via


Passo a passo: Multiplicação matricial

Este passo a passo demonstra como usar o C++ AMP para acelerar a execução da multiplicação de matrizes. Dois algoritmos são apresentados, um sem segmentação e outro com segmentação.

Pré-requisitos

Antes de começar:

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.

Para criar o projeto

As instruções para criar um novo projeto variam dependendo de qual versão do Visual Studio você instalou. Para ver a documentação da sua versão preferida do Visual Studio, use o controlador de seleção Versão . Encontra-se na parte superior do índice desta página.

Para criar o projeto no Visual Studio

  1. Na barra de menus, escolha Arquivo>Novo>Projeto para abrir a caixa de diálogo Criar um Novo Projeto .

  2. Na parte superior da caixa de diálogo, defina Language como C++, defina Platform como Windows e defina Project type como Console.

  3. Na lista filtrada de tipos de projeto, escolha Projeto vazio e, em seguida, escolha Avançar. Na próxima página, digite MatrixMultiply na caixa Nome para especificar um nome para o projeto e especifique o local do projeto, se desejado.

    Captura de tela mostrando a caixa de diálogo Criar um novo projeto com o modelo Aplicativo de Console selecionado.

  4. Escolha o botão Criar para criar o projeto cliente.

  5. No Gerenciador de Soluções, abra o menu de atalho para Arquivos de Origem e escolha Adicionar>Novo Item.

  6. Na caixa de diálogo Adicionar Novo Item , selecione Arquivo C++ (.cpp), digite MatrixMultiply.cpp na caixa Nome e escolha o botão Adicionar .

Para criar um projeto no Visual Studio 2017 ou 2015

  1. Na barra de menus do Visual Studio, escolha Arquivo>Novo>Projeto.

  2. Em Instalado no painel de modelos, selecione Visual C++.

  3. Selecione Projeto vazio, digite MatrixMultiply na caixa Nome e escolha o botão OK .

  4. Escolha o botão Next .

  5. No Gerenciador de Soluções, abra o menu de atalho para Arquivos de Origem e escolha Adicionar>Novo Item.

  6. 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 pavimentação

Nesta seção, considere a multiplicação de duas matrizes, A e B, que são definidas da seguinte forma:

Diagrama mostrando 3 por 2 matriz A.

Diagrama mostrando 2 por 3 matriz B.

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 as linhas de A pelas colunas de B elemento por elemento.

Diagrama mostrando o resultado 3 por 3 da matriz de produtos.

Para multiplicar sem usar C++ AMP

  1. Abra MatrixMultiply.cpp e use o código a seguir 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";
        }
    }
    
    int main() {
        MultiplyWithOutAMP();
        getchar();
    }
    

    O algoritmo é uma implementação direta da definição de multiplicação matricial. Ele não usa nenhum algoritmo paralelo ou encadeado para reduzir o tempo de computação.

  2. Na barra de menus, escolha Arquivo>Salvar tudo.

  3. Escolha o atalho de teclado F5 para iniciar a depuração e verifique se a saída está correta.

  4. Escolha Enter para sair do aplicativo.

Para multiplicar usando C++ AMP

  1. No MatrixMultiply.cpp, adicione o seguinte código antes do main método.

    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 é semelhante ao código não-AMP. A chamada para parallel_for_each inicia um thread para cada elemento no product.extente substitui os for loops de linha e coluna. O valor da célula na linha e coluna está disponível em idx. Você pode acessar os elementos de um array_view objeto 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 array_view::synchronize método copia os product valores da variável de volta para a productMatrix variável.

  2. Adicione as seguintes instruções include e using na parte superior de MatrixMultiply.cpp.

    #include <amp.h>
    using namespace concurrency;
    
  3. Modifique o main método para chamar o MultiplyWithAMP método.

    int main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        getchar();
    }
    
  4. Pressione o atalho de teclado Ctrl+F5 para iniciar a depuração e verificar se a saída está correta.

  5. Pressione a barra de espaço para sair do aplicativo.

Multiplicação com mosaicos

A técnica de mosaico consiste em particionar dados em subconjuntos de tamanho igual, conhecidos como tiles. Três coisas mudam quando usas revestimento.

  • Você pode criar tile_static variáveis. O acesso aos dados no tile_static espaço pode ser muitas vezes mais rápido do que o acesso aos dados no espaço global. Uma instância de uma tile_static variável é criada para cada bloco, e todos os threads no bloco têm acesso à variável. O principal benefício do azulejamento é o ganho de desempenho devido ao tile_static acesso.

  • Você pode chamar o método tile_barrier::wait para parar todos os encadeamentos num tile numa linha de código específica. Você não pode garantir a ordem em que os threads serão executados, apenas que todos os threads em um bloco pararão na chamada para tile_barrier::wait antes de continuarem a execução.

  • Você tem acesso ao índice da linha de execução relativo ao objeto inteiro array_view e ao índice relativo ao tile. Usando o índice local, você pode tornar seu código mais fácil de ler e depurar.

Para tirar proveito do particionamento em blocos na multiplicação de matrizes, o algoritmo deve dividir a matriz em azulejos e, em seguida, copiar os dados desses azulejos em tile_static variáveis 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 seu produto neste exemplo são:

Diagrama mostrando 4 por 4 matriz A.

Diagrama mostrando a matriz B 4 por 4.

Diagrama mostrando o resultado 4 por 4 da matriz de produtos.

As matrizes são particionadas em quatro matrizes 2x2, que são definidas da seguinte forma:

Diagrama mostrando a matriz A 4 por 4 particionada em submatrizes 2 por 2.

Diagrama mostrando 4 por 4 matriz B particionada em 2 por 2 submatrizes.

O produto de A e B pode agora ser escrito e calculado da seguinte forma:

Diagrama mostrando 4 por 4 matriz A B particionada em 2 por 2 submatrizes.

Como as matrizes de a a h são matrizes 2x2, todos os produtos e somas delas também são matrizes 2x2. Segue-se também que o produto de A e B é uma matriz 4x4, como esperado. Para verificar rapidamente o algoritmo, calcule o valor do elemento na primeira linha, primeira coluna do produto. No exemplo, esse seria o valor do elemento na primeira linha e na primeira coluna de ae + bg. Só tem de calcular a primeira coluna e a primeira linha de ae e bg para cada termo. Esse valor para ae é (1 * 1) + (2 * 5) = 11. O valor para bg é (3 * 1) + (4 * 5) = 23. O valor final é 11 + 23 = 34, o que está correto.

Para implementar esse algoritmo, o código:

  • Usa um tiled_extent objeto em vez de um extent objeto na chamada parallel_for_each.

  • Usa um tiled_index objeto em vez de um index objeto na chamada parallel_for_each.

  • Cria tile_static variáveis para manter as submatrizes.

  • Usa o método tile_barrier::wait para suspender as threads que calculam os produtos das submatrizes.

Para multiplicar usando AMP e mosaico

  1. No MatrixMultiply.cpp, adicione o seguinte código antes do main método.

    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 revestimento. O código usa estas etapas conceituais:

    1. Copie os elementos de tile[0,0] de a em locA. Copie os elementos de tile[0,0] de b em locB. Observe que product está lado a lado, não a e b. Portanto, você usa índices globais para acessar a, be product. A chamada para tile_barrier::wait é essencial. Pára todos os processos no bloco até que ambos locA e locB estejam preenchidos.

    2. Multiplique locA e locB coloque os resultados em product.

    3. Copie os elementos de tile[0,1] de a em locA. Copie os elementos do mosaico [1,0] de b em locB.

    4. Multiplique locA e locB adicione-os aos resultados que já estão em product.

    5. A multiplicação da telha[0,0] está completa.

    6. Repita para os outros quatro azulejos. Não há indexação especificamente para os blocos e os threads podem ser executados em qualquer ordem. À medida que cada thread é executada, as variáveis tile_static são criadas adequadamente para cada bloco, e a chamada para tile_barrier::wait controla o fluxo do programa.

    7. Ao examinar o algoritmo de perto, observe que cada submatriz é carregada em uma tile_static memória duas vezes. Essa transferência de dados leva tempo. No entanto, uma vez que os dados estão na tile_static memória, o acesso aos dados é muito mais rápido. Como o cálculo dos produtos requer acesso repetido aos valores nas submatrizes, há um ganho de desempenho global. Para cada algoritmo, a experimentação é necessária para encontrar o algoritmo ideal e o tamanho do bloco.

    Nos exemplos não-AMP e não-tile, cada elemento de A e B é acessado quatro vezes a partir da memória global para calcular o produto. No exemplo de bloco, cada elemento é acessado duas vezes da memória global e quatro vezes da memória tile_static. Não se trata de um ganho de desempenho significativo. No entanto, se as matrizes A e B fossem 1024x1024 e o tamanho da telha fosse 16, haveria um ganho de desempenho significativo. Nesse caso, cada elemento seria copiado na tile_static memória apenas 16 vezes e acessado da tile_static memória 1024 vezes.

  2. Modifique o método principal para chamar o MultiplyWithTiling método, conforme mostrado.

    int main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        MultiplyWithTiling();
        getchar();
    }
    
  3. Pressione o atalho de teclado Ctrl+F5 para iniciar a depuração e verificar se a saída está correta.

  4. Pressione a barra de espaço para sair do aplicativo.

Ver também

C++ AMP (paralelismo maciço acelerado em C++)
Guia: Depuração de uma Aplicação AMP C++