Condividi tramite


Utilizzo di sezioni

Puoi usare la tiling per ottimizzare l'accelerazione della tua app. L'affiancamento divide i thread in sottoinsiemi rettangolari o riquadri uguali. Se si usa una dimensione del riquadro e un algoritmo affiancato appropriati, è possibile ottenere ancora più accelerazione dal codice AMP C++. I componenti di base della tiling sono:

  • tile_static Variabili. Il vantaggio principale della tiling è il miglioramento delle prestazioni dall'accesso tile_static . L'accesso ai dati in tile_static memoria può essere notevolmente più veloce rispetto all'accesso ai dati nello spazio globale (array o array_view oggetti ). Viene creata un'istanza di una tile_static variabile per ogni riquadro e tutti i thread nel riquadro hanno accesso alla variabile. In un tipico algoritmo affiancato, i dati vengono copiati in tile_static memoria una volta dalla memoria globale e quindi a cui si accede molte volte dalla tile_static memoria.

  • Metodo tile_barrier::wait. Una chiamata a tile_barrier::wait sospende l'esecuzione del thread corrente fino a quando tutti i thread nello stesso riquadro raggiungono la chiamata a tile_barrier::wait. Non è possibile garantire l'ordine in cui verranno eseguiti i thread, ma solo che nessun thread nel riquadro eseguirà oltre la chiamata a tile_barrier::wait finché tutti i thread non hanno raggiunto la chiamata. Ciò significa che usando il tile_barrier::wait metodo è possibile eseguire attività in base al riquadro anziché a thread per thread. Un tipico algoritmo di collegamento include codice per inizializzare la tile_static memoria per l'intero riquadro seguito da una chiamata a tile_barrier::wait. Il codice seguente tile_barrier::wait contiene calcoli che richiedono l'accesso a tutti i tile_static valori.

  • Indicizzazione locale e globale. È possibile accedere all'indice del thread rispetto all'intero array_view oggetto o array e all'indice relativo al riquadro. L'uso dell'indice locale consente di semplificare la lettura e il debug del codice. In genere, si usa l'indicizzazione locale per accedere tile_static alle variabili e l'indicizzazione globale per accedere alle array variabili e array_view .

  • classe tiled_extent e classe tiled_index. Si usa un tiled_extent oggetto anziché un extent oggetto nella parallel_for_each chiamata. Si usa un tiled_index oggetto anziché un index oggetto nella parallel_for_each chiamata.

Per sfruttare i vantaggi dell'associazione, l'algoritmo deve partizionare il dominio di calcolo in riquadri e quindi copiare i dati del riquadro in tile_static variabili per un accesso più rapido.

Esempio di indici globali, affiancati e locali

Nota

Le intestazioni C++ AMP sono deprecate a partire da Visual Studio 2022 versione 17.0. L'inclusione di eventuali intestazioni AMP genererà errori di compilazione. Definire _SILENCE_AMP_DEPRECATION_WARNINGS prima di includere eventuali intestazioni AMP per disattivare gli avvisi.

Il diagramma seguente rappresenta una matrice di dati 8x9 disposta in riquadri 2x3.

Diagramma di una matrice di 8 per 9 divisa in 2 per 3 riquadri.

Nell'esempio seguente vengono visualizzati gli indici globali, affiancati e locali di questa matrice affiancata. Un array_view oggetto viene creato utilizzando elementi di tipo Description. Description contiene gli indici globali, affiancati e locali dell'elemento nella matrice. Il codice nella chiamata a parallel_for_each imposta i valori degli indici globali, affiancati e locali di ogni elemento. L'output visualizza i valori nelle Description strutture.

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

Il lavoro principale dell'esempio è nella definizione dell'oggetto array_view e nella chiamata a parallel_for_each.

  1. Il vettore delle Description strutture viene copiato in un oggetto 8x9 array_view .

  2. Il parallel_for_each metodo viene chiamato con un tiled_extent oggetto come dominio di calcolo. L'oggetto tiled_extent viene creato chiamando il extent::tile() metodo della descriptions variabile. I parametri di tipo della chiamata a extent::tile(), <2,3>, specificano che vengono creati riquadri 2x3. La matrice 8x9 viene quindi affiancata in 12 riquadri, quattro righe e tre colonne.

  3. Il parallel_for_each metodo viene chiamato utilizzando un tiled_index<2,3> oggetto (t_idx) come indice. I parametri di tipo dell'indice (t_idx) devono corrispondere ai parametri di tipo del dominio di calcolo (descriptions.extent.tile< 2, 3>()).

  4. Quando ogni thread viene eseguito, l'indice t_idx restituisce informazioni sul riquadro in cui si trova il thread (tiled_index::tile proprietà) e sulla posizione del thread all'interno del riquadro (tiled_index::local proprietà ).

Sincronizzazione dei riquadri: tile_static e tile_barrier::wait

L'esempio precedente illustra il layout e gli indici dei riquadri, ma non è molto utile. L'affiancamento diventa utile quando i riquadri sono integrali all'algoritmo e sfruttano tile_static le variabili. Poiché tutti i thread in un riquadro hanno accesso alle tile_static variabili, le chiamate a tile_barrier::wait vengono usate per sincronizzare l'accesso alle tile_static variabili. Anche se tutti i thread in un riquadro hanno accesso alle tile_static variabili, non esiste un ordine di esecuzione garantito dei thread nel riquadro. Nell'esempio seguente viene illustrato come usare tile_static le variabili e il tile_barrier::wait metodo per calcolare il valore medio di ogni riquadro. Ecco le chiavi per comprendere l'esempio:

  1. RawData viene archiviato in una matrice 8x8.

  2. La dimensione del riquadro è 2x2. In questo modo viene creata una griglia 4x4 di riquadri e le medie possono essere archiviate in una matrice 4x4 usando un array oggetto . Esistono solo un numero limitato di tipi che è possibile acquisire per riferimento in una funzione con restrizioni AMP. La array classe è una di esse.

  3. Le dimensioni della matrice e le dimensioni del campione vengono definite usando #define istruzioni , perché i parametri di tipo in array, extentarray_view, e tiled_index devono essere valori costanti. È anche possibile usare const int static le dichiarazioni. Come vantaggio aggiuntivo, è semplice modificare le dimensioni del campione per calcolare la media su 4x4 riquadri.

  4. Per ogni riquadro viene dichiarata una tile_static matrice di valori float 2x2. Anche se la dichiarazione si trova nel percorso del codice per ogni thread, viene creata una sola matrice per ogni riquadro nella matrice.

  5. È presente una riga di codice per copiare i valori in ogni riquadro nella tile_static matrice. Per ogni thread, dopo che il valore viene copiato nella matrice, l'esecuzione nel thread si arresta a causa della chiamata a tile_barrier::wait.

  6. Quando tutti i thread in un riquadro hanno raggiunto la barriera, è possibile calcolare la media. Poiché il codice viene eseguito per ogni thread, è presente un'istruzione if per calcolare solo la media in un thread. La media viene archiviata nella variabile media. La barriera è essenzialmente il costrutto che controlla i calcoli in base al riquadro, in quanto è possibile usare un for ciclo.

  7. I dati nella averages variabile, perché si tratta di un array oggetto , devono essere copiati di nuovo nell'host. In questo esempio viene usato l'operatore di conversione vettoriale.

  8. Nell'esempio completo è possibile impostare SAMPLESIZE su 4 e il codice viene eseguito correttamente senza altre modifiche.

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

Condizioni di traccia

Potrebbe essere possibile creare una tile_static variabile denominata total e incrementare tale variabile per ogni thread, come illustrato di seguito:

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

Il primo problema con questo approccio è che tile_static le variabili non possono avere inizializzatori. Il secondo problema è che esiste una race condition nell'assegnazione a total, perché tutti i thread nel riquadro hanno accesso alla variabile in nessun ordine specifico. È possibile programmare un algoritmo per consentire a un solo thread di accedere al totale in ogni barriera, come illustrato di seguito. Tuttavia, questa soluzione non è estendibile.

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

Recinzioni di memoria

Esistono due tipi di accesso alla memoria che devono essere sincronizzati, ovvero l'accesso alla memoria globale e tile_static l'accesso alla memoria. Un concurrency::array oggetto alloca solo la memoria globale. Un concurrency::array_view oggetto può fare riferimento a memoria globale, tile_static memoria o entrambi, a seconda della modalità di costruzione. Esistono due tipi di memoria che devono essere sincronizzati:

  • memoria globale

  • tile_static

Un limite di memoria garantisce che gli accessi alla memoria siano disponibili per altri thread nel riquadro del thread e che gli accessi alla memoria vengano eseguiti in base all'ordine del programma. Per garantire questo problema, i compilatori e i processori non riordinano le letture e le scritture attraverso il recinto. In C++ AMP, una recinzione di memoria viene creata da una chiamata a uno di questi metodi:

  • metodo tile_barrier::wait: crea un recinto intorno sia alla memoria globale tile_static che alla memoria.

  • Metodo tile_barrier::wait_with_all_memory_fence: crea un recinto sia globale tile_static che di memoria.

  • Metodo tile_barrier::wait_with_global_memory_fence: crea un recinto intorno solo alla memoria globale.

  • Metodo tile_barrier::wait_with_tile_static_memory_fence: crea un recinto intorno solo tile_static alla memoria.

La chiamata al limite specifico necessario può migliorare le prestazioni dell'app. Il tipo di barriera influisce sul modo in cui il compilatore e le istruzioni di riordinamento hardware. Ad esempio, se si usa un limite di memoria globale, si applica solo agli accessi alla memoria globale e pertanto il compilatore e l'hardware potrebbero riordinare le letture e scrivere in tile_static variabili sui due lati del recinto.

Nell'esempio seguente la barriera sincronizza le scritture in tileValues, una tile_static variabile . In questo esempio viene tile_barrier::wait_with_tile_static_memory_fence chiamato anziché 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);
    }
});

Vedi anche

C++ AMP (C++ Accelerated Massive Parallelism)
Parola chiave tile_static