Użycie fragmentów
Możesz użyć tilingu, aby zmaksymalizować przyspieszenie aplikacji. Tiling dzieli wątki na równe prostokątne podzbiory lub kafelki. Jeśli używasz odpowiedniego rozmiaru kafelka i algorytmu kafelka, możesz uzyskać jeszcze więcej przyspieszenia z kodu C++ AMP. Podstawowe składniki tilingu to:
tile_static
Zmiennych. Główną zaletą tilingu jest uzyskanie dostępu dotile_static
wydajności. Dostęp do danych wtile_static
pamięci może być znacznie szybszy niż dostęp do danych w przestrzeni globalnej (array
lubarray_view
obiektów). Wystąpienie zmiennejtile_static
jest tworzone dla każdego kafelka, a wszystkie wątki na kafelku mają dostęp do zmiennej. W typowym algorytmie kafelków dane są kopiowane dotile_static
pamięci raz z pamięci globalnej, a następnie uzyskiwane wiele razytile_static
z pamięci.tile_barrier::wait, metoda. Wywołanie w celu
tile_barrier::wait
wstrzymania wykonywania bieżącego wątku do momentu, aż wszystkie wątki w tym samym kafelku dotrą dotile_barrier::wait
wywołania metody . Nie można zagwarantować kolejności uruchamiania wątków, tylko że żadne wątki na kafelku nie będą wykonywane obok wywołania, dopókitile_barrier::wait
wszystkie wątki nie osiągną wywołania. Oznacza to, że przy użyciutile_barrier::wait
metody można wykonywać zadania na podstawie kafelka po kafelku, a nie na podstawie wątku po wątku. Typowy algorytm tilinga zawiera kod inicjowaniatile_static
pamięci dla całego kafelka, po którym następuje wywołanie metodytile_barrier::wait
.tile_barrier::wait
Poniższy kod zawiera obliczenia wymagające dostępu do wszystkichtile_static
wartości.Indeksowanie lokalne i globalne. Masz dostęp do indeksu wątku względem całego
array_view
obiektu lubarray
i indeksu względem kafelka. Użycie indeksu lokalnego może ułatwić odczytywanie i debugowanie kodu. Zazwyczaj indeksowanie lokalne służy do uzyskiwania dostępu do zmiennych oraz indeksowania globalnego w celu uzyskaniatile_static
dostępu doarray
zmiennych iarray_view
zmiennych.klasa tiled_extent i klasa tiled_index. W wywołaniu należy użyć
tiled_extent
obiektu zamiastextent
obiektu.parallel_for_each
W wywołaniu należy użyćtiled_index
obiektu zamiastindex
obiektu.parallel_for_each
Aby móc korzystać z tilingu, algorytm musi podzielić domenę obliczeniową na kafelki, a następnie skopiować dane kafelka do tile_static
zmiennych, aby uzyskać szybszy dostęp.
Przykład indeksów globalnych, kafelków i lokalnych
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.
Na poniższym diagramie przedstawiono macierz 8x9 danych rozmieszczonych na 2x3 kafelkach.
Poniższy przykład przedstawia globalne, kafelki i lokalne indeksy tej macierzy kafelków. Obiekt array_view
jest tworzony przy użyciu elementów typu Description
. Zawiera Description
globalne, kafelki i lokalne indeksy elementu w macierzy. Kod w wywołaniu ustawia parallel_for_each
wartości globalnych, kafelków i lokalnych indeksów każdego elementu. Dane wyjściowe wyświetlają wartości w strukturach Description
.
#include <iostream>
#include <iomanip>
#include <Windows.h>
#include <amp.h>
using namespace concurrency;
const int ROWS = 8;
const int COLS = 9;
// tileRow and tileColumn specify the tile that each thread is in.
// globalRow and globalColumn specify the location of the thread in the array_view.
// localRow and localColumn specify the location of the thread relative to the tile.
struct Description {
int value;
int tileRow;
int tileColumn;
int globalRow;
int globalColumn;
int localRow;
int localColumn;
};
// A helper function for formatting the output.
void SetConsoleColor(int color) {
int colorValue = (color == 0) 4 : 2;
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colorValue);
}
// A helper function for formatting the output.
void SetConsoleSize(int height, int width) {
COORD coord;
coord.X = width;
coord.Y = height;
SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coord);
SMALL_RECT* rect = new SMALL_RECT();
rect->Left = 0;
rect->Top = 0;
rect->Right = width;
rect->Bottom = height;
SetConsoleWindowInfo(GetStdHandle(STD_OUTPUT_HANDLE), true, rect);
}
// This method creates an 8x9 matrix of Description structures.
// In the call to parallel_for_each, the structure is updated
// with tile, global, and local indices.
void TilingDescription() {
// Create 72 (8x9) Description structures.
std::vector<Description> descs;
for (int i = 0; i < ROWS * COLS; i++) {
Description d = {i, 0, 0, 0, 0, 0, 0};
descs.push_back(d);
}
// Create an array_view from the Description structures.
extent<2> matrix(ROWS, COLS);
array_view<Description, 2> descriptions(matrix, descs);
// Update each Description with the tile, global, and local indices.
parallel_for_each(descriptions.extent.tile< 2, 3>(),
[=] (tiled_index< 2, 3> t_idx) restrict(amp)
{
descriptions[t_idx].globalRow = t_idx.global[0];
descriptions[t_idx].globalColumn = t_idx.global[1];
descriptions[t_idx].tileRow = t_idx.tile[0];
descriptions[t_idx].tileColumn = t_idx.tile[1];
descriptions[t_idx].localRow = t_idx.local[0];
descriptions[t_idx].localColumn= t_idx.local[1];
});
// Print out the Description structure for each element in the matrix.
// Tiles are displayed in red and green to distinguish them from each other.
SetConsoleSize(100, 150);
for (int row = 0; row < ROWS; row++) {
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Value: " << std::setw(2) << descriptions(row, column).value << " ";
}
std::cout << "\n";
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Tile: " << "(" << descriptions(row, column).tileRow << "," << descriptions(row, column).tileColumn << ") ";
}
std::cout << "\n";
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Global: " << "(" << descriptions(row, column).globalRow << "," << descriptions(row, column).globalColumn << ") ";
}
std::cout << "\n";
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Local: " << "(" << descriptions(row, column).localRow << "," << descriptions(row, column).localColumn << ") ";
}
std::cout << "\n";
std::cout << "\n";
}
}
int main() {
TilingDescription();
char wait;
std::cin >> wait;
}
Główną pracą przykładu jest definicja array_view
obiektu i wywołanie metody parallel_for_each
.
Wektor
Description
struktur jest kopiowany do obiektu 8x9array_view
.Metoda
parallel_for_each
jest wywoływana z obiektem jako domeną obliczeniowątiled_extent
. Obiekttiled_extent
jest tworzony przez wywołanieextent::tile()
metody zmiennejdescriptions
. Parametry typu wywołania metodyextent::tile()
,<2,3>
określ, że są tworzone kafelki 2x3. W związku z tym macierz 8x9 jest kafelek na 12 kafelków, cztery wiersze i trzy kolumny.Metoda
parallel_for_each
jest wywoływana przy użyciutiled_index<2,3>
obiektu (t_idx
) jako indeksu. Parametry typu indeksu (t_idx
) muszą być zgodne z parametrami typu domeny obliczeniowej (descriptions.extent.tile< 2, 3>()
).Po wykonaniu każdego wątku indeks
t_idx
zwraca informacje o tym, który kafelek wątek znajduje się w obiekcie (tiled_index::tile
właściwość) i lokalizację wątku w kafelku (tiled_index::local
właściwość).
Synchronizacja kafelków — tile_static i tile_barrier::wait
W poprzednim przykładzie pokazano układ kafelka i indeksy, ale nie jest on bardzo przydatny. Tiling staje się przydatny, gdy kafelki są integralną częścią algorytmu i wykorzystują tile_static
zmienne. Ponieważ wszystkie wątki na kafelku mają dostęp do tile_static
zmiennych, wywołania są tile_barrier::wait
używane do synchronizowania dostępu do tile_static
zmiennych. Mimo że wszystkie wątki na kafelku mają dostęp do tile_static
zmiennych, nie ma gwarantowanej kolejności wykonywania wątków na kafelku. W poniższym przykładzie pokazano, jak używać tile_static
zmiennych i tile_barrier::wait
metody do obliczania średniej wartości każdego kafelka. Poniżej przedstawiono klucze do zrozumienia przykładu:
RawData jest przechowywana w macierzy 8x8.
Rozmiar kafelka to 2x2. Spowoduje to utworzenie siatki 4x4 kafelków, a średnie mogą być przechowywane w macierzy 4x4 przy użyciu
array
obiektu. Istnieje tylko ograniczona liczba typów, które można przechwycić za pomocą odwołania w funkcji z ograniczeniami AMP. Klasaarray
jest jedną z nich.Rozmiar macierzy i rozmiar próbki są definiowane przy użyciu
#define
instrukcji, ponieważ parametry typu doarray
,array_view
extent
, itiled_index
muszą być wartościami stałymi. Można również użyćconst int static
deklaracji. Dodatkową korzyścią jest banalna zmiana rozmiaru próbki w celu obliczenia średniej ponad 4x4 kafelków.Dla każdego kafelka zadeklarowana jest tablica
tile_static
2x2 wartości zmiennoprzecinkowych. Mimo że deklaracja znajduje się w ścieżce kodu dla każdego wątku, tylko jedna tablica jest tworzona dla każdego kafelka w macierzy.Istnieje wiersz kodu umożliwiający skopiowanie wartości w każdym kafelku do tablicy
tile_static
. Dla każdego wątku po skopiowaniu wartości do tablicy wykonanie wątku zostanie zatrzymane z powodu wywołania metodytile_barrier::wait
.Gdy wszystkie wątki na kafelku osiągnęły barierę, można obliczyć średnią. Ponieważ kod jest wykonywany dla każdego wątku, istnieje
if
instrukcja, aby obliczyć średnią tylko dla jednego wątku. Średnia jest przechowywana w zmiennej averages. Bariera jest zasadniczo konstrukcją, która steruje obliczeniami według kafelka, podobnie jak pętlafor
.Dane w zmiennej
averages
, ponieważ jestarray
to obiekt, muszą zostać skopiowane z powrotem do hosta. W tym przykładzie użyto operatora konwersji wektorów.W pełnym przykładzie można zmienić parametr SAMPLESIZE na 4, a kod jest wykonywany poprawnie bez żadnych innych zmian.
#include <iostream>
#include <amp.h>
using namespace concurrency;
#define SAMPLESIZE 2
#define MATRIXSIZE 8
void SamplingExample() {
// Create data and array_view for the matrix.
std::vector<float> rawData;
for (int i = 0; i < MATRIXSIZE * MATRIXSIZE; i++) {
rawData.push_back((float)i);
}
extent<2> dataExtent(MATRIXSIZE, MATRIXSIZE);
array_view<float, 2> matrix(dataExtent, rawData);
// Create the array for the averages.
// There is one element in the output for each tile in the data.
std::vector<float> outputData;
int outputSize = MATRIXSIZE / SAMPLESIZE;
for (int j = 0; j < outputSize * outputSize; j++) {
outputData.push_back((float)0);
}
extent<2> outputExtent(MATRIXSIZE / SAMPLESIZE, MATRIXSIZE / SAMPLESIZE);
array<float, 2> averages(outputExtent, outputData.begin(), outputData.end());
// Use tiles that are SAMPLESIZE x SAMPLESIZE.
// Find the average of the values in each tile.
// The only reference-type variable you can pass into the parallel_for_each call
// is a concurrency::array.
parallel_for_each(matrix.extent.tile<SAMPLESIZE, SAMPLESIZE>(),
[=, &averages] (tiled_index<SAMPLESIZE, SAMPLESIZE> t_idx) restrict(amp)
{
// Copy the values of the tile into a tile-sized array.
tile_static float tileValues[SAMPLESIZE][SAMPLESIZE];
tileValues[t_idx.local[0]][t_idx.local[1]] = matrix[t_idx];
// Wait for the tile-sized array to load before you calculate the average.
t_idx.barrier.wait();
// If you remove the if statement, then the calculation executes for every
// thread in the tile, and makes the same assignment to averages each time.
if (t_idx.local[0] == 0 && t_idx.local[1] == 0) {
for (int trow = 0; trow < SAMPLESIZE; trow++) {
for (int tcol = 0; tcol < SAMPLESIZE; tcol++) {
averages(t_idx.tile[0],t_idx.tile[1]) += tileValues[trow][tcol];
}
}
averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE * SAMPLESIZE);
}
});
// Print out the results.
// You cannot access the values in averages directly. You must copy them
// back to a CPU variable.
outputData = averages;
for (int row = 0; row < outputSize; row++) {
for (int col = 0; col < outputSize; col++) {
std::cout << outputData[row*outputSize + col] << " ";
}
std::cout << "\n";
}
// Output for SAMPLESIZE = 2 is:
// 4.5 6.5 8.5 10.5
// 20.5 22.5 24.5 26.5
// 36.5 38.5 40.5 42.5
// 52.5 54.5 56.5 58.5
// Output for SAMPLESIZE = 4 is:
// 13.5 17.5
// 45.5 49.5
}
int main() {
SamplingExample();
}
Warunki wyścigu
Może być kuszące utworzenie zmiennej tile_static
o nazwie total
i zwiększanie tej zmiennej dla każdego wątku w następujący sposób:
// Do not do this.
tile_static float total;
total += matrix[t_idx];
t_idx.barrier.wait();
averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE* SAMPLESIZE);
Pierwszym problemem z tym podejściem jest to, że tile_static
zmienne nie mogą mieć inicjatorów. Drugim problemem jest to, że istnieje warunek wyścigu dla przypisania do total
, ponieważ wszystkie wątki na kafelku mają dostęp do zmiennej w żadnej określonej kolejności. Algorytm można zaprogramować tak, aby zezwalał tylko jednemu wątkowi na dostęp do sumy w każdej barierze, jak pokazano poniżej. Jednak to rozwiązanie nie jest rozszerzalne.
// Do not do this.
tile_static float total;
if (t_idx.local[0] == 0&& t_idx.local[1] == 0) {
total = matrix[t_idx];
}
t_idx.barrier.wait();
if (t_idx.local[0] == 0&& t_idx.local[1] == 1) {
total += matrix[t_idx];
}
t_idx.barrier.wait();
// etc.
Ogrodzenia pamięci
Istnieją dwa rodzaje dostępu do pamięci, które muszą być zsynchronizowane — globalny dostęp do pamięci i tile_static
dostęp do pamięci. Obiekt concurrency::array
przydziela tylko pamięć globalną. Element concurrency::array_view
może odwoływać się do pamięci globalnej, tile_static
pamięci lub obu tych elementów w zależności od sposobu jego konstruowania. Istnieją dwa rodzaje pamięci, które muszą być zsynchronizowane:
pamięć globalna
tile_static
Ogrodzenie pamięci zapewnia, że dostęp do pamięci jest dostępny dla innych wątków na kafelku wątku, a dostęp do pamięci jest wykonywany zgodnie z kolejnością programu. Aby to zapewnić, kompilatory i procesory nie zmieniają kolejności operacji odczytu i zapisu w płocie. W języku C++ AMP ogrodzenie pamięci jest tworzone przez wywołanie jednej z następujących metod:
tile_barrier::wait— Metoda: tworzy ogrodzenie wokół pamięci i globalnej
tile_static
.tile_barrier::wait_with_all_memory_fence Metoda: tworzy ogrodzenie zarówno wokół pamięci globalnej, jak i
tile_static
pamięci.tile_barrier::wait_with_global_memory_fence Metoda: tworzy ogrodzenie tylko dla pamięci globalnej.
tile_barrier::wait_with_tile_static_memory_fence Metoda: tworzy ogrodzenie tylko
tile_static
wokół pamięci.
Wywoływanie określonego ogrodzenia, którego potrzebujesz, może zwiększyć wydajność aplikacji. Typ bariery wpływa na sposób kompilatora i instrukcji zmiany kolejności sprzętu. Jeśli na przykład używasz globalnego ogrodzenia pamięci, ma zastosowanie tylko do dostępu do pamięci globalnej, a w związku z tym kompilator i sprzęt mogą zmienić kolejność operacji odczytu i zapisu w tile_static
zmiennych po obu stronach ogrodzenia.
W następnym przykładzie bariera synchronizuje zapisy z tileValues
zmienną tile_static
. W tym przykładzie tile_barrier::wait_with_tile_static_memory_fence
jest wywoływana nazwa tile_barrier::wait
zamiast .
// Using a tile_static memory fence.
parallel_for_each(matrix.extent.tile<SAMPLESIZE, SAMPLESIZE>(),
[=, &averages] (tiled_index<SAMPLESIZE, SAMPLESIZE> t_idx) restrict(amp)
{
// Copy the values of the tile into a tile-sized array.
tile_static float tileValues[SAMPLESIZE][SAMPLESIZE];
tileValues[t_idx.local[0]][t_idx.local[1]] = matrix[t_idx];
// Wait for the tile-sized array to load before calculating the average.
t_idx.barrier.wait_with_tile_static_memory_fence();
// If you remove the if statement, then the calculation executes
// for every thread in the tile, and makes the same assignment to
// averages each time.
if (t_idx.local[0] == 0&& t_idx.local[1] == 0) {
for (int trow = 0; trow <SAMPLESIZE; trow++) {
for (int tcol = 0; tcol <SAMPLESIZE; tcol++) {
averages(t_idx.tile[0],t_idx.tile[1]) += tileValues[trow][tcol];
}
}
averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE* SAMPLESIZE);
}
});
Zobacz też
C++ AMP (C++ Accelerated Massive Parallelism)
tile_static, słowo kluczowe