Procédure pas à pas : Multiplication des matrices

Cette procédure pas à pas montre comment utiliser C++ AMP pour accélérer l’exécution de la multiplication de matrices. Deux algorithmes sont présentés, un sans mosaïques et un avec mosaïques.

Prérequis

Avant de commencer :

Remarque

Les en-têtes AMP C++ sont déconseillés à partir de Visual Studio 2022 version 17.0. L’inclusion d’en-têtes AMP génère des erreurs de génération. Définissez _SILENCE_AMP_DEPRECATION_WARNINGS avant d’inclure tous les en-têtes AMP pour silence les avertissements.

Pour créer le projet

Les instructions de création d’un projet varient en fonction de la version de Visual Studio que vous avez installée. Pour consulter la documentation sur votre version préférée de Visual Studio, utilisez le contrôle de sélection de Version . Il se trouve en haut de la table des matières de cette page.

Pour créer le projet dans Visual Studio

  1. Dans la barre de menus, choisissez Fichier>Nouveau>Projet pour ouvrir la boîte de dialogue Créer un projet.

  2. En haut de la boîte de dialogue, définissez Langage sur C++, Plateforme sur Windows et Type de projet sur Console.

  3. Dans la liste filtrée des types de projets, choisissez Projet vide, puis Suivant. Dans la page suivante, entrez MatrixMultiply dans la zone Nom pour spécifier un nom pour le projet, puis spécifiez l’emplacement du projet si vous le souhaitez.

    Screenshot showing the Create a new project dialog with the Console App template selected.

  4. Choisissez le bouton Créer pour créer le projet client.

  5. Dans Explorateur de solutions, ouvrez le menu contextuel des fichiers sources, puis choisissez Ajouter>un nouvel élément.

  6. Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Fichier C++ (.cpp), entrez MatrixMultiply.cpp dans la zone Nom , puis choisissez le bouton Ajouter .

Pour créer un projet dans Visual Studio 2017 ou 2015

  1. Dans la barre de menus de Visual Studio, choisissez Fichier>Nouveau>Projet.

  2. Sous Installé dans le volet modèles, sélectionnez Visual C++.

  3. Sélectionnez Projet vide, entrez MatrixMultiply dans la zone Nom , puis choisissez le bouton OK .

  4. Choisissez le bouton Suivant.

  5. Dans Explorateur de solutions, ouvrez le menu contextuel des fichiers sources, puis choisissez Ajouter>un nouvel élément.

  6. Dans la boîte de dialogue Ajouter un nouvel élément , sélectionnez Fichier C++ (.cpp), entrez MatrixMultiply.cpp dans la zone Nom , puis choisissez le bouton Ajouter .

Multiplication sans mosaïcage

Dans cette section, tenez compte de la multiplication de deux matrices, A et B, qui sont définies comme suit :

Diagram showing 3 by 2 matrix A.

Diagram showing 2 by 3 matrix B.

A est une matrice de 3 par 2 et B est une matrice de 2 par 3. Le produit de la multiplication de A par B est la matrice suivante de 3 à 3. Le produit est calculé en multipliant les lignes d’A par les colonnes de l’élément B par élément.

Diagram showing the result 3 by 3 product matrix.

Pour multiplier sans utiliser L’AMP C++

  1. Ouvrez MatrixMultiply.cpp et utilisez le code suivant pour remplacer le code existant.

    #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();
    }
    

    L’algorithme est une implémentation simple de la définition de la multiplication de matrices. Il n’utilise aucun algorithme parallèle ou thread pour réduire le temps de calcul.

  2. Dans la barre de menus, sélectionnez Fichier>Enregistrer tout.

  3. Choisissez le raccourci clavier F5 pour démarrer le débogage et vérifiez que la sortie est correcte.

  4. Choisissez Entrée pour quitter l’application.

Pour multiplier à l’aide de C++ AMP

  1. Dans MatrixMultiply.cpp, ajoutez le code suivant avant la main méthode.

    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";
      }
    }
    

    Le code AMP ressemble au code non AMP. L’appel à parallel_for_each démarrer un thread pour chaque élément dans product.extent, remplace les for boucles de ligne et de colonne. La valeur de la cellule à la ligne et à la colonne est disponible dans idx. Vous pouvez accéder aux éléments d’un array_view objet à l’aide de l’opérateur [] et d’une variable d’index, ou de l’opérateur () et des variables de ligne et de colonne. L’exemple illustre les deux méthodes. La array_view::synchronize méthode copie les valeurs de la product variable vers la productMatrix variable.

  2. Ajoutez les instructions suivantes include en using haut de MatrixMultiply.cpp.

    #include <amp.h>
    using namespace concurrency;
    
  3. Modifiez la main méthode pour appeler la MultiplyWithAMP méthode.

    int main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        getchar();
    }
    
  4. Appuyez sur le raccourci clavier Ctrl+F5 pour démarrer le débogage et vérifier que la sortie est correcte.

  5. Appuyez sur La barre d’espace pour quitter l’application.

Multiplication avec mosaïcage

Le tiling est une technique dans laquelle vous partitionnez des données en sous-ensembles de taille égale, appelées vignettes. Trois choses changent lorsque vous utilisez le mosaïme.

  • Vous pouvez créer des tile_static variables. L’accès aux données dans tile_static l’espace peut être plusieurs fois plus rapide que l’accès aux données dans l’espace global. Une instance d’une tile_static variable est créée pour chaque vignette, et tous les threads de la vignette ont accès à la variable. Le principal avantage de la mosaïne est le gain de performances en raison de l’accès tile_static .

  • Vous pouvez appeler la méthode tile_barrier ::wait pour arrêter tous les threads d’une vignette à une ligne de code spécifiée. Vous ne pouvez pas garantir l’ordre dans lequel les threads s’exécutent, uniquement que tous les threads d’une vignette s’arrêtent à l’appel tile_barrier::wait avant qu’ils continuent l’exécution.

  • Vous avez accès à l’index du thread par rapport à l’objet entier array_view et à l’index par rapport à la vignette. En utilisant l’index local, vous pouvez faciliter la lecture et le débogage de votre code.

Pour tirer parti de la mosaïque en multiplication de matrices, l’algorithme doit partitionner la matrice en vignettes, puis copier les données de vignette dans des tile_static variables pour un accès plus rapide. Dans cet exemple, la matrice est partitionnée en sous-entités de taille égale. Le produit est trouvé en multipliant les submatrices. Les deux matrices et leur produit dans cet exemple sont les suivants :

Diagram showing 4 by 4 matrix A.

Diagram showing 4 by 4 matrix B.

Diagram showing result 4 by 4 product matrix.

Les matrices sont partitionnées en quatre matrices 2x2, qui sont définies comme suit :

Diagram showing 4 by 4 matrix A partitioned into 2 by 2 sub matrices.

Diagram showing 4 by 4 matrix B partitioned into 2 by 2 sub matrices.

Le produit de A et B peut maintenant être écrit et calculé comme suit :

Diagram showing 4 by 4 matrix A B partitioned into 2 by 2 sub matrices.

Étant donné que les matrices a à travers h sont des matrices 2x2, tous les produits et les sommes d’entre eux sont également des matrices 2x2. Il suit également que le produit de A et B est une matrice 4x4, comme prévu. Pour case activée rapidement l’algorithme, calculez la valeur de l’élément dans la première ligne, première colonne du produit. Dans l’exemple, il s’agirait de la valeur de l’élément dans la première ligne et la première colonne de ae + bg. Vous devez uniquement calculer la première colonne, la première ligne et aebg pour chaque terme. Cette valeur est ae(1 * 1) + (2 * 5) = 11. La valeur pour bg laquelle est (3 * 1) + (4 * 5) = 23. La valeur finale est 11 + 23 = 34, qui est correcte.

Pour implémenter cet algorithme, le code :

  • Utilise un tiled_extent objet au lieu d’un extent objet dans l’appel parallel_for_each .

  • Utilise un tiled_index objet au lieu d’un index objet dans l’appel parallel_for_each .

  • Crée des tile_static variables pour contenir les sous-atrices.

  • Utilise la tile_barrier::wait méthode pour arrêter les threads pour le calcul des produits des submatrices.

Pour multiplier à l’aide d’AMP et de mosaïne

  1. Dans MatrixMultiply.cpp, ajoutez le code suivant avant la main méthode.

    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";
        }
    }
    

    Cet exemple est sensiblement différent de l’exemple sans mosaïne. Le code utilise ces étapes conceptuelles :

    1. Copiez les éléments de la vignette[0,0] dans alocA. Copiez les éléments de la vignette[0,0] dans blocB. Notez qu’il product est en mosaïque, pas a et b. Par conséquent, vous utilisez des index globaux pour accéder a, bà , et product. L’appel est tile_barrier::wait essentiel. Il arrête tous les threads de la vignette jusqu’à ce que les deux et locB soient locA remplis.

    2. Multipliez et placez locA les résultats en product.locB

    3. Copiez les éléments de la vignette[0,1] dans alocA. Copiez les éléments de la vignette [1,0] dans blocB.

    4. Multipliez locA et locB ajoutez-les aux résultats déjà présents product.

    5. La multiplication de la vignette[0,0] est terminée.

    6. Répétez pour les quatre autres vignettes. Il n’existe aucune indexation spécifique pour les vignettes et les threads peuvent s’exécuter dans n’importe quel ordre. À mesure que chaque thread s’exécute, les tile_static variables sont créées pour chaque vignette de manière appropriée et l’appel aux tile_barrier::wait contrôles du flux de programme.

    7. Lorsque vous examinez étroitement l’algorithme, notez que chaque sous-matrix est chargé dans une tile_static mémoire deux fois. Ce transfert de données prend du temps. Toutefois, une fois que les données sont en tile_static mémoire, l’accès aux données est beaucoup plus rapide. Étant donné que le calcul des produits nécessite un accès répété aux valeurs des submatrices, il existe un gain global de performances. Pour chaque algorithme, l’expérimentation est nécessaire pour trouver l’algorithme optimal et la taille de vignette.

    Dans les exemples non AMP et non-vignettes, chaque élément de A et B est accessible quatre fois à partir de la mémoire globale pour calculer le produit. Dans l’exemple de vignette, chaque élément est accessible deux fois à partir de la mémoire globale et quatre fois à partir de la tile_static mémoire. Ce n’est pas un gain de performance significatif. Toutefois, si les matrices A et B étaient de 1024 x 1024 et que la taille des mosaïques était de 16, il y aurait un gain de performances significatif. Dans ce cas, chaque élément est copié en tile_static mémoire seulement 16 fois et accessible à partir de tile_static la mémoire 1024 fois.

  2. Modifiez la méthode principale pour appeler la MultiplyWithTiling méthode, comme indiqué.

    int main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        MultiplyWithTiling();
        getchar();
    }
    
  3. Appuyez sur le raccourci clavier Ctrl+F5 pour démarrer le débogage et vérifier que la sortie est correcte.

  4. Appuyez sur la barre d’espace pour quitter l’application.

Voir aussi

C++ AMP (C++ Accelerated Massive Parallelism)
Procédure pas-à-pas : débogage d’une application C++ AMP