Wskazówki: mnożenie macierzy

W tym przewodniku krok po kroku pokazano, jak używać języka C++ AMP w celu przyspieszenia wykonywania mnożenia macierzy. Prezentowane są dwa algorytmy: jeden bez układania i jeden z tilingiem.

Wymagania wstępne

Przed rozpoczęciem:

Uwaga

Nagłówki C++ AMP są przestarzałe, począwszy od programu Visual Studio 2022 w wersji 17.0. Dołączenie wszystkich nagłówków AMP spowoduje wygenerowanie błędów kompilacji. Zdefiniuj _SILENCE_AMP_DEPRECATION_WARNINGS przed dołączeniem żadnych nagłówków AMP, aby wyciszyć ostrzeżenia.

Aby utworzyć projekt

Instrukcje dotyczące tworzenia nowego projektu różnią się w zależności od zainstalowanej wersji programu Visual Studio. Aby zapoznać się z dokumentacją preferowanej wersji programu Visual Studio, użyj kontrolki selektora wersji . Znajduje się on w górnej części spisu treści na tej stronie.

Aby utworzyć projekt w programie Visual Studio

  1. Na pasku menu wybierz pozycję Plik>nowy>projekt, aby otworzyć okno dialogowe Tworzenie nowego projektu.

  2. W górnej części okna dialogowego ustaw wartość Language na C++, ustaw wartość Platforma na Windows, a następnie ustaw wartość Project type (Typ projektu) na Console (Konsola).

  3. Z filtrowanej listy typów projektów wybierz pozycję Pusty projekt , a następnie wybierz pozycję Dalej. Na następnej stronie wprowadź wartość MatrixMultiply w polu Nazwa , aby określić nazwę projektu i w razie potrzeby określić lokalizację projektu.

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

  4. Wybierz przycisk Utwórz, aby utworzyć projekt klienta.

  5. W Eksplorator rozwiązań otwórz menu skrótów dla plików źródłowych, a następnie wybierz pozycję Dodaj>nowy element.

  6. W oknie dialogowym Dodawanie nowego elementu wybierz pozycję Plik C++ (cpp), wprowadź wartość MatrixMultiply.cpp w polu Nazwa, a następnie wybierz przycisk Dodaj.

Aby utworzyć projekt w programie Visual Studio 2017 lub 2015

  1. Na pasku menu w programie Visual Studio wybierz pozycję Plik>nowy>projekt.

  2. W obszarze Zainstalowane w okienku szablonów wybierz pozycję Visual C++.

  3. Wybierz pozycję Pusty projekt, wprowadź wartość MatrixMultiply w polu Nazwa, a następnie wybierz przycisk OK.

  4. Wybierz przycisk Dalej.

  5. W Eksplorator rozwiązań otwórz menu skrótów dla plików źródłowych, a następnie wybierz pozycję Dodaj>nowy element.

  6. W oknie dialogowym Dodawanie nowego elementu wybierz pozycję Plik C++ (cpp), wprowadź wartość MatrixMultiply.cpp w polu Nazwa, a następnie wybierz przycisk Dodaj.

Mnożenie bez tilingu

W tej sekcji rozważ mnożenie dwóch macierzy, A i B, które są zdefiniowane w następujący sposób:

Diagram showing 3 by 2 matrix A.

Diagram showing 2 by 3 matrix B.

A jest macierzą 3 po 2, a B jest macierzą 2 po 3. Pomnożenie A przez B jest następującą macierzą 3-do-3. Produkt jest obliczany przez pomnożenie wierszy elementu A przez kolumny elementu B według elementu.

Diagram showing the result 3 by 3 product matrix.

Aby pomnożyć bez użycia języka C++ AMP

  1. Otwórz plik MatrixMultiply.cpp i zastąp istniejący kod poniższym kodem.

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

    Algorytm jest prostą implementacją definicji mnożenia macierzy. Nie używa żadnych algorytmów równoległych ani wątkowych w celu skrócenia czasu obliczeń.

  2. Na pasku menu wybierz pozycję Plik>Zapisz wszystko.

  3. Wybierz skrót klawiaturowy F5 , aby rozpocząć debugowanie i sprawdzić, czy dane wyjściowe są poprawne.

  4. Wybierz klawisz Enter , aby zamknąć aplikację.

Aby pomnożyć przy użyciu języka C++ AMP

  1. W pliku MatrixMultiply.cpp dodaj następujący kod przed main metodą .

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

    Kod AMP przypomina kod inny niż AMP. Wywołanie uruchamia parallel_for_each jeden wątek dla każdego elementu w product.extentelemencie i zastępuje for pętle dla wierszy i kolumn. Wartość komórki w wierszu i kolumnie jest dostępna w pliku idx. Dostęp do elementów array_view obiektu można uzyskać przy użyciu [] operatora i zmiennej indeksu albo () operatora oraz zmiennych wierszy i kolumn. W przykładzie przedstawiono obie metody. Metoda array_view::synchronize kopiuje wartości zmiennej product z powrotem do zmiennej productMatrix .

  2. Dodaj następujące include instrukcje i using w górnej części pliku MatrixMultiply.cpp.

    #include <amp.h>
    using namespace concurrency;
    
  3. Zmodyfikuj metodę , main aby wywołać metodę MultiplyWithAMP .

    int main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        getchar();
    }
    
  4. Naciśnij skrót klawiaturowy Ctrl+F5, aby rozpocząć debugowanie i sprawdzić, czy dane wyjściowe są poprawne.

  5. Naciśnij spację, aby zamknąć aplikację.

Mnożenie z tilingiem

Tiling to technika, w której dzielisz dane na podzestawy o równym rozmiarze, które są nazywane kafelkami. Trzy rzeczy zmieniają się podczas korzystania z tilingu.

  • Można tworzyć tile_static zmienne. Dostęp do danych w tile_static przestrzeni może być wielokrotnie szybszy niż dostęp do danych w przestrzeni globalnej. Wystąpienie zmiennej tile_static jest tworzone dla każdego kafelka, a wszystkie wątki na kafelku mają dostęp do zmiennej. Główną zaletą układania płytek jest wzrost wydajności ze względu na tile_static dostęp.

  • Możesz wywołać metodę tile_barrier::wait , aby zatrzymać wszystkie wątki w jednym kafelku w określonym wierszu kodu. Nie można zagwarantować kolejności uruchamiania wątków, tylko że wszystkie wątki w jednym kafelku zatrzymają się przed tile_barrier::wait kontynuowaniem wykonywania.

  • Masz dostęp do indeksu wątku względem całego array_view obiektu i indeksu względem kafelka. Korzystając z indeksu lokalnego, możesz ułatwić odczytywanie i debugowanie kodu.

Aby móc korzystać z tilingu w mnożeniu macierzy, algorytm musi podzielić macierz na kafelki, a następnie skopiować dane kafelka do tile_static zmiennych, aby uzyskać szybszy dostęp. W tym przykładzie macierz jest podzielona na macierze o równym rozmiarze. Produkt można znaleźć przez pomnożenie podmatry. Dwa macierze i ich produkt w tym przykładzie to:

Diagram showing 4 by 4 matrix A.

Diagram showing 4 by 4 matrix B.

Diagram showing result 4 by 4 product matrix.

Macierze są podzielone na cztery macierze 2x2, które są zdefiniowane w następujący sposób:

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.

Produkt A i B można teraz napisać i obliczyć w następujący sposób:

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

Ponieważ macierze a przez h to 2x2 macierze, wszystkie produkty i sumy z nich są również 2x2 macierze. Wynika to również z tego, że produkt A i B jest macierzą 4x4, zgodnie z oczekiwaniami. Aby szybko sprawdzić algorytm, oblicz wartość elementu w pierwszym wierszu, pierwszą kolumnę w produkcie. W tym przykładzie będzie to wartość elementu w pierwszym wierszu i pierwszej kolumnie .ae + bg Musisz obliczyć tylko pierwszą kolumnę, pierwszy wiersz ae i bg dla każdego terminu. Ta wartość dla ae parametru to (1 * 1) + (2 * 5) = 11. Wartość parametru bg to (3 * 1) + (4 * 5) = 23. Końcowa wartość to 11 + 23 = 34, która jest poprawna.

Aby zaimplementować ten algorytm, kod:

  • tiled_extent Używa obiektu zamiast extent obiektu w wywołaniu parallel_for_each .

  • tiled_index Używa obiektu zamiast index obiektu w wywołaniu parallel_for_each .

  • Tworzy tile_static zmienne do przechowywania macierzy podrzędnych.

  • tile_barrier::wait Używa metody , aby zatrzymać wątki na potrzeby obliczania produktów macierzy podrzędnych.

Aby pomnożyć przy użyciu amp i tiling

  1. W pliku MatrixMultiply.cpp dodaj następujący kod przed main metodą .

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

    Ten przykład jest znacznie inny niż w przykładzie bez tilingu. Kod używa następujących kroków koncepcyjnych:

    1. Skopiuj elementy kafelka[0,0] z a elementu do locA. Skopiuj elementy kafelka[0,0] z b elementu do locB. Zwróć uwagę, że product jest to kafelek, a nie a .b W związku z tym do uzyskiwania dostępu a, bdo parametrów i productnależy użyć indeksów globalnych. Wezwanie do tile_barrier::wait jest niezbędne. Spowoduje to zatrzymanie wszystkich wątków na kafelku do momentu wypełnienia obu locA tych elementów.locB

    2. Pomnóż locA i locB umieść wyniki w pliku product.

    3. Skopiuj elementy kafelka[0,1] z a elementu do elementu locA. Skopiuj elementy kafelka [1,0] z b elementu do locB.

    4. Pomnóż locA i locB dodaj je do wyników, które znajdują się już w pliku product.

    5. Mnożenie kafelka[0,0] zostało ukończone.

    6. Powtórz dla pozostałych czterech kafelków. Nie ma indeksowania specjalnie dla kafelków, a wątki mogą być wykonywane w dowolnej kolejności. Gdy każdy wątek jest wykonywany, tile_static zmienne są tworzone odpowiednio dla każdego kafelka i wywołanie do tile_barrier::wait sterowania przepływem programu.

    7. Podczas dokładnego badania algorytmu zwróć uwagę, że każdy podmatrix jest ładowany do tile_static pamięci dwa razy. Transfer danych zajmuje trochę czasu. Jednak gdy dane są w tile_static pamięci, dostęp do danych jest znacznie szybszy. Ponieważ obliczanie produktów wymaga wielokrotnego dostępu do wartości w podmatrych, istnieje ogólny wzrost wydajności. W przypadku każdego algorytmu eksperymentowanie jest wymagane do znalezienia optymalnego algorytmu i rozmiaru kafelka.

    W przykładach innych niż AMP i innych niż kafelki każdy element A i B jest dostępny cztery razy z pamięci globalnej w celu obliczenia produktu. W przykładzie kafelka każdy element jest uzyskiwany dwa razy z pamięci globalnej i cztery razy z tile_static pamięci. To nie jest znaczący wzrost wydajności. Jeśli jednak macierze A i B były 1024x1024, a rozmiar kafelka był 16, byłby znaczący wzrost wydajności. W takim przypadku każdy element zostanie skopiowany do tile_static pamięci tylko 16 razy i uzyskiwany do nich dostęp z tile_static pamięci 1024 razy.

  2. Zmodyfikuj metodę main, aby wywołać metodę MultiplyWithTiling , jak pokazano poniżej.

    int main() {
        MultiplyWithOutAMP();
        MultiplyWithAMP();
        MultiplyWithTiling();
        getchar();
    }
    
  3. Naciśnij skrót klawiaturowy Ctrl+F5, aby rozpocząć debugowanie i sprawdzić, czy dane wyjściowe są poprawne.

  4. Naciśnij pasek spacji, aby zamknąć aplikację.

Zobacz też

C++ AMP (C++ Accelerated Massive Parallelism)
Przewodnik: debugowanie aplikacji C++ AMP