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:
- Przeczytaj omówienie C++ AMP.
- Odczytywanie przy użyciu kafelków.
- Upewnij się, że używasz co najmniej systemu Windows 7 lub Windows Server 2008 R2.
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
Na pasku menu wybierz pozycję Plik>nowy>projekt, aby otworzyć okno dialogowe Tworzenie nowego projektu.
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).
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.
Wybierz przycisk Utwórz, aby utworzyć projekt klienta.
W Eksplorator rozwiązań otwórz menu skrótów dla plików źródłowych, a następnie wybierz pozycję Dodaj>nowy element.
W oknie dialogowym Dodawanie nowego elementu wybierz pozycję Plik C++ (.cpp), wprowadź MatrixMultiply.cpp w polu Nazwa, a następnie wybierz przycisk Dodaj.
Aby utworzyć projekt w programie Visual Studio 2017 lub 2015
Na pasku menu w programie Visual Studio wybierz pozycję Plik>nowy>projekt.
W obszarze Zainstalowane w okienku szablonów wybierz pozycję Visual C++.
Wybierz pozycję Pusty projekt, wprowadź wartość MatrixMultiply w polu Nazwa, a następnie wybierz przycisk OK.
Wybierz przycisk Dalej.
W Eksplorator rozwiązań otwórz menu skrótów dla plików źródłowych, a następnie wybierz pozycję Dodaj>nowy element.
W oknie dialogowym Dodawanie nowego elementu wybierz pozycję Plik C++ (.cpp), wprowadź 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:
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.
Aby pomnożyć bez użycia języka C++ AMP
Otwórz MatrixMultiply.cpp i użyj następującego kodu, aby zastąpić istniejący kod.
#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ń.
Na pasku menu wybierz pozycję Plik>Zapisz wszystko.
Wybierz skrót klawiaturowy F5 , aby rozpocząć debugowanie i sprawdzić, czy dane wyjściowe są poprawne.
Wybierz Enter , aby zamknąć aplikację.
Aby pomnożyć przy użyciu języka C++ AMP
W 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 wproduct.extent
elemencie i zastępujefor
pętle dla wierszy i kolumn. Wartość komórki w wierszu i kolumnie jest dostępna w plikuidx
. Dostęp do elementówarray_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. Metodaarray_view::synchronize
kopiuje wartości zmiennejproduct
z powrotem do zmiennejproductMatrix
.Dodaj następujące
include
instrukcje iusing
w górnej części MatrixMultiply.cpp.#include <amp.h> using namespace concurrency;
Zmodyfikuj metodę ,
main
aby wywołać metodęMultiplyWithAMP
.int main() { MultiplyWithOutAMP(); MultiplyWithAMP(); getchar(); }
Naciśnij skrót klawiaturowy Ctrl+F5, aby rozpocząć debugowanie i sprawdzić, czy dane wyjściowe są poprawne.
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 wtile_static
przestrzeni może być wielokrotnie szybszy niż dostęp do danych w przestrzeni globalnej. Wystąpienie zmiennejtile_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 natile_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:
Macierze są podzielone na cztery macierze 2x2, które są zdefiniowane w następujący sposób:
Produkt A i B można teraz napisać i obliczyć w następujący sposób:
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 zamiastextent
obiektu w wywołaniuparallel_for_each
.tiled_index
Używa obiektu zamiastindex
obiektu w wywołaniuparallel_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
W 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:
Skopiuj elementy kafelka[0,0] z
a
elementu dolocA
. Skopiuj elementy kafelka[0,0] zb
elementu dolocB
. Zwróć uwagę, żeproduct
jest to kafelek, a niea
.b
W związku z tym do uzyskiwania dostępua, b
do parametrów iproduct
należy użyć indeksów globalnych. Wezwanie dotile_barrier::wait
jest niezbędne. Spowoduje to zatrzymanie wszystkich wątków na kafelku do momentu wypełnienia obulocA
tych elementów.locB
Pomnóż
locA
ilocB
umieść wyniki w plikuproduct
.Skopiuj elementy kafelka[0,1] z
a
elementu do elementulocA
. Skopiuj elementy kafelka [1,0] zb
elementu dolocB
.Pomnóż
locA
ilocB
dodaj je do wyników, które znajdują się już w plikuproduct
.Mnożenie kafelka[0,0] zostało ukończone.
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 dotile_barrier::wait
sterowania przepływem programu.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ą wtile_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 dotile_static
pamięci tylko 16 razy i uzyskiwany do nich dostęp ztile_static
pamięci 1024 razy.Zmodyfikuj metodę main, aby wywołać metodę
MultiplyWithTiling
, jak pokazano poniżej.int main() { MultiplyWithOutAMP(); MultiplyWithAMP(); MultiplyWithTiling(); getchar(); }
Naciśnij skrót klawiaturowy Ctrl+F5, aby rozpocząć debugowanie i sprawdzić, czy dane wyjściowe są poprawne.
Naciśnij pasek spacji, aby zamknąć aplikację.
Zobacz też
C++ AMP (C++ Accelerated Massive Parallelism)
Przewodnik: debugowanie aplikacji C++ AMP