Za pomocą płytki
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żna uzyskać jeszcze większe przyśpieszenie kodu C++ AMP.Podstawowe składniki fragmentacji to:
tile_static zmienne.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.
Metoda tile_barrier::wait.Wywołanie metody 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 tylko to, ż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 w stosunku do całego obiektu array_view lub array i do indeksu względem 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 i indeksowanie globalne w celu uzyskania dostępu do zmiennych array i array_view.
Klasa tiled_extent i Klasa tiled_index.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 rozmieszczone 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;
cons tint 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 a 4x4 matrix of Description structures. In the call to parallel_for_each, the structure is updated
// with tile, global, and local indices.
void TilingDescription() {
// Create 16 (4x4) 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łówne praca przykładu jest wykonywana w definicji obiektu array_view i wywołaniu metody 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 metody 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żna również użyć deklaracji const int static.Dodatkową korzyścią jest możliwość trywialnej zmiany rozmiaru próbki do obliczania średniej ponad rozmiar 4x4 fragmentów.
Zadeklarowano tablicę 2x2 wartości zmiennoprzecinkowych tile_static 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 tego, aby obliczenie średniej nastąpiło tylko w jednym wątku.Średnia jest przechowywana w zmiennej averages.Bariera to zasadniczo konstrukcja kontrolująca obliczenia dla każdego fragmentu, część z was może użyć pętli for.
Dane w zmiennej averages, ponieważ 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 zmiennej total, ponieważ wszystkie wątki we fragmencie posiadają dostęp do zmiennej w losowej kolejności.Można 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:
Metoda tile_barrier::wait: Tworzy horyzont zarówno wokół pamięci globalnej jak i tile_static.
Metoda tile_barrier::wait_with_all_memory_fence: Tworzy horyzont zarówno wokół pamięci globalnej jak i tile_static.
Metoda tile_barrier::wait_with_global_memory_fence: Tworzy horyzont tylko wokół pamięci globalnej.
Metoda tile_barrier::wait_with_tile_static_memory_fence: 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życia horyzontu pamięci globalnej, dotyczy to tylko dostęp do pamięci globalnej i dlatego kompilator i sprzęt może zmienić 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);
}
});