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 do tile_static wydajności. Dostęp do danych w tile_static pamięci może być znacznie szybszy niż dostęp do danych w przestrzeni globalnej (array lub array_view obiektów). Wystąpienie zmiennej tile_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 do tile_static pamięci raz z pamięci globalnej, a następnie uzyskiwane wiele razy tile_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ą do tile_barrier::waitwywoł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óki tile_barrier::wait wszystkie wątki nie osiągną wywołania. Oznacza to, że przy użyciu tile_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 inicjowania tile_static pamięci dla całego kafelka, po którym następuje wywołanie metody tile_barrier::wait. tile_barrier::wait Poniższy kod zawiera obliczenia wymagające dostępu do wszystkich tile_static wartości.

  • Indeksowanie lokalne i globalne. Masz dostęp do indeksu wątku względem całego array_view obiektu lub array 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 uzyskania tile_static dostępu do array zmiennych i array_view zmiennych.

  • klasa tiled_extent i klasa tiled_index. W wywołaniu należy użyć tiled_extent obiektu zamiast extent obiektu.parallel_for_each W wywołaniu należy użyć tiled_index obiektu zamiast index 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.

Diagram of an 8 by 9 matrix divided into 2 by 3 tiles.

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.

  1. Wektor Description struktur jest kopiowany do obiektu 8x9 array_view .

  2. Metoda parallel_for_each jest wywoływana z obiektem jako domeną obliczeniową tiled_extent . Obiekt tiled_extent jest tworzony przez wywołanie extent::tile() metody zmiennej descriptions . Parametry typu wywołania metody extent::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.

  3. Metoda parallel_for_each jest wywoływana przy użyciu tiled_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>()).

  4. 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:

  1. RawData jest przechowywana w macierzy 8x8.

  2. 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. Klasa array jest jedną z nich.

  3. Rozmiar macierzy i rozmiar próbki są definiowane przy użyciu #define instrukcji, ponieważ parametry typu do array, array_viewextent, i tiled_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.

  4. 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.

  5. 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 metody tile_barrier::wait.

  6. 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ętla for .

  7. Dane w zmiennej averages , ponieważ jest array to obiekt, muszą zostać skopiowane z powrotem do hosta. W tym przykładzie użyto operatora konwersji wektorów.

  8. 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:

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 tileValueszmienną tile_static . W tym przykładzie tile_barrier::wait_with_tile_static_memory_fence jest wywoływana nazwa tile_barrier::waitzamiast .

// 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