Udostępnij za pośrednictwem


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.

Macierz 8 przez 9 podzielony Kafelki 2 przez 3

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.

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

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

  3. 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>()).

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

  1. Obiekt rawData jest przechowywany w macierzy 8x8.

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

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

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

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

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

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

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

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);
    }
});

Zobacz też

Informacje

tile_static słowa kluczowego

Inne zasoby

C++ AMP (C++ Accelerated Massive Parallelism)