Użycie fragmentów
Można używać fragmentacji, aby zmaksymalizować przyspieszenie aplikacji.Fragmentacja dzieli wątki na równe podzestawy prostokątne lub fragmenty.Używając odpowiedniego rozmiaru fragmentu oraz algorytmu fragmentacji możesz uzyskać jeszcze większe przyśpieszenie kodu C++ AMP.Podstawowe składniki fragmentacji to:
Zmienne tile_static.Podstawową zaletą fragmentacji jest przyrost wydajności związany z dostępem do tile_static.Dostęp do danych w pamięci tile_static może być znacznie szybszy niż dostęp do danych w przestrzeni globalnej (obiekty array lub array_view).Wystąpienie zmiennej tile_static jest tworzone dla każdego fragmentu i wszystkie wątki we fragmencie mają dostęp do zmiennej.W typowym algorytmie fragmentacji, dane są kopiowane do pamięci tile_static z globalnej pamięci, a następnie uzyskuje się do nich wielokrotny dostęp z pamięci tile_static.
tile_barrier::wait — Metoda.Wywołanie tile_barrier::wait zawiesza wykonywanie bieżącego wątku, dopóki wszystkie wątki w tym samym fragmencie nie osiągną wywołania tile_barrier::wait.Nie można zagwarantować porządku, w jakim wątki będą uruchamiane, można jedynie zagwarantować, że żadne wątki we fragmencie nie zostaną wykonane po wywołaniu metody tile_barrier::wait , dopóki wszystkie wątki nie osiągną wywołania.Oznacza to, że za pomocą metody tile_barrier::wait można wykonywać zadania stosując zasadę fragment po fragmencie zamiast wątek po wątku.Typowy algorytm fragmentacji posiada kod do inicjowania pamięci tile_static dla całego fragmentu, poprzedzony wywołaniem tile_barrer::wait.Kod następujący po tile_barrier::wait zawiera obliczenia, które wymagają dostępu do wszystkich wartości tile_static.
Lokalne i globalne indeksowanie.Masz dostęp do indeksu wątku dla całego obiektu array_view lub array oraz do indeksu dla fragmentu.Używanie lokalnego indeksu, może uczynić kod łatwiejszym do odczytywania i debugowania.Zazwyczaj używane jest indeksowanie lokalne w celu uzyskania dostępu do zmiennych tile_static, oraz indeksowanie globalne w celu uzyskania dostępu do zmiennych array i array_view.
tiled_extent — Klasa i tiled_index — Klasa.Używasz obiektu tiled_extent zamiast obiektu extent w wywołaniu parallel_for_each.Używasz obiektu tiled_index zamiast obiektu index w wywołaniu parallel_for_each.
Aby skorzystać z fragmentacji, algorytm musi podzielić domenę obliczeniową na fragmenty, a następnie skopiować dane z fragmentów do zmiennych tile_static , aby uzyskać do nich szybszy dostęp.
Przykład indeksowania globalnego, fragmentarycznego oraz lokalnego
Poniższy diagram przedstawia macierz danych 8x9 we fragmentach 2x3.
Poniższy przykład pokazuje indeksowanie globalne, fragmentaryczne i lokalne tej macierzy fragmentów.Obiekt array_view jest tworzony przy użyciu elementów typu Description.Element Description przechowuje indeksy globalne, fragmentaryczne oraz lokalne elementów macierzy.Kod w wywołaniu parallel_for_each ustawia wartości indeksów globalnych, fragmentarycznych oraz lokalnych 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";
}
}
void main() {
TilingDescription();
char wait;
std::cin >> wait;
}
Główna praca przykładu jest wykonywana w definicji obiektu array_view i wywołaniu do parallel_for_each.
Wektor struktur Description jest kopiowany do obiektu array_view o rozmiarze 8x9.
Metoda parallel_for_each jest wywoływana z obiektem tiled_extent jako domena obliczająca.Obiekt tiled_extent jest tworzony przez wywołanie metody extent::tile() zmiennej descriptions.Parametry typu wywołania metody extent::tile(), <2,3>, określają, że tworzone są fragmenty o rozmiarze 2x3.W ten sposób macierz 8x9 jest dzielona na 12 fragmentów, cztery wiersze i trzy kolumny.
Metoda parallel_for_each jest wywoływana poprzez użycie obiektu tiled_index<2,3>, (t_idx) jako indeks.Parametry typu indeksu (t_idx) muszą odpowiadać parametrom typu domeny obliczającej (descriptions.extent.tile< 2, 3>()).
Przy wykonywaniu każdego wątku, indeks t_idx zwraca informacje o tym, w którym fragmencie znajduje się wątek: (właściwość tiled_index::tile) i informacje o lokalizacji wątku we fragmencie (właściwość tiled_index::local).
Synchronizacja—tile_static i tile_barrier::wait
Poprzedni przykład ilustruje układ fragmentów i wskaźniki, ale sam w sobie nie jest bardzo przydatny. Fragmentacja jest użyteczna, gdy fragmenty są integralną częścią algorytmów i wykorzystują zmienne tile_static.Ponieważ wszystkie wątki we fragmencie mają dostęp do zmiennych tile_static, wywołania do tile_barrier::wait są używane do synchronizowania dostępu do zmiennych tile_static.Pomimo że wszystkie wątki we fragmencie mają dostęp do zmiennych tile_static, nie jest zagwarantowana kolejność wykonywania wątków we fragmencie.Poniższy przykład pokazuje, jak używać zmiennych tile_static i metody tile_barrier::wait w celu obliczenia średniej wartości każdego fragmentu.Kluczowe założenia niezbędne do zrozumienia przykładu są następujące:
Obiekt rawData jest przechowywany w macierzy 8x8.
Rozmiar fragmentu to 2x2.Tworzy siatkę (4x4) fragmentów, a średnie mogą być przechowywane w macierzy 4x4 za pomocą obiektu array.Istnieje ograniczona liczba typów, które można przechwycić poprzez odwołanie w funkcji z ograniczeniami AMP.Klasa array jest jedną z nich.
Rozmiar macierzy i rozmiar próbki są definiowane za pomocą instrukcji #define, ponieważ parametry typu array, array_view, extent i tiled_index muszą być wartościami stałymi.Możesz również użyć deklaracji const int static.Dodatkowo niezwykle łatwo można przeprowadzić zmianę rozmiaru próbki do obliczania średniej ponad rozmiar 4x4 fragmentów.
Zadeklarowano tablicę tile_static 2x2 wartości zmiennoprzecinkowych dla każdego fragmentu.Pomimo że deklaracja znajduje się w ścieżce kodu dla każdego wątku, tylko jedna tablica jest tworzona dla każdego fragmentu w macierzy.
Istnieje linia kodu pozwalająca kopiować wartości z każdego fragmentu do tablicy tile_static.Dla każdego wątku, po skopiowaniu wartości do tablicy, wykonanie wątku zatrzymuje ze względu na wywołanie tile_barrier::wait.
Jeśli wszystkie wątki we fragmencie osiągnęły barierę, można obliczyć średnią.Ponieważ kod jest wykonywany dla każdego wątku, występuje instrukcja if służąca do obliczenia średniej w jednym wątku.Średnia jest przechowywana w zmiennej averages.Bariera to zasadniczo konstrukcja kontrolująca obliczenia dla każdego fragmentu, w podobnym celu można użyć pętli for.
Dane w zmiennej averages, jako że jest to obiekt array, muszą zostać skopiowane z powrotem do hosta.W tym przykładzie został użyty operator konwersji wektorowej.
W kompletnym przykładzie można zmienić SAMPLESIZE na 4 i kod zostanie wykonywany prawidłowo 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 SAMPLESSIZE = 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();
}
Sytuacje wyścigu
Może być kuszące utworzenie zmiennej tile_static o nazwie total i zwiększanie wartość 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);
Pierwszy problem w tym podejściu związany jest z tym, że zmienne tile_static nie mogą mieć inicjatorów.Drugi problem to sytuacja wyścigu przy przypisaniu do total, ponieważ wszystkie wątki we fragmencie posiadają dostęp do zmiennej w losowej kolejności.Możesz napisać algorytm zezwalający tylko jednemu wątkowi na dostęp do zmiennej total przy każdej barierze, jak pokazano dalej.To rozwiązanie nie jest jednak 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.
Horyzonty pamięci
Istnieją dwa rodzaje dostępów do pamięci, które muszą być synchronizowane — dostęp do pamięci globalnej oraz dostęp do pamięci tile_static.Obiekt concurrency::array przydziela tylko pamięć globalną.Obiekt concurrency::array_view może odwoływać się do globalnej pamięci, pamięci tile_static, lub obu, w zależności od tego, jak został zbudowany. Istnieją dwa rodzaje pamięci, które muszą być synchronizowane:
pamięć globalna
tile_static
Obiekt Horyzont pamięci zapewnia, że dostęp do pamięci jest dostępny dla innych wątków we fragmencie wątków i że dostępy do pamięci są wykonywane w założonej kolejności.Aby to zapewnić kompilatory oraz procesory nie zmieniają kolejności odczytów i zapisów w ramach horyzontu.W C++ AMP horyzont pamięci jest tworzony przez wywołanie jednej z następujących metod:
tile_barrier::wait — Metoda: Tworzy horyzont zarówno wokół pamięci globalnej, jak i tile_static.
tile_barrier::wait_with_all_memory_fence — Metoda: Tworzy horyzont zarówno wokół pamięci globalnej, jak i tile_static.
tile_barrier::wait_with_global_memory_fence — Metoda: Tworzy horyzont tylko wokół pamięci globalnej.
tile_barrier::wait_with_tile_static_memory_fence — Metoda: Tworzy horyzont tylko wokół pamięci tile_static.
Wywoływanie określonego, wymaganego horyzontu może zwiększyć wydajność aplikacji użytkownika.Typ bariery wpływa na ułożenie instrukcji przez kompilator i sprzęt.Na przykład, w przypadku używania horyzontu pamięci globalnej, dotyczy to tylko dostępu do pamięci globalnej i dlatego kompilator i sprzęt mogą zmieniać kolejność operacji odczytu i zapisu zmiennych tile_static po obu stronach horyzontu.
W następnym przykładzie bariera synchronizuje zapisy do tileValues i zmiennej tile_static.W tym przykładzie jest wywołana metoda tile_barrier::wait_with_tile_static_memory_fence zamiast tile_barrier::wait.
// 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);
}
});