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'accessotile_static
. L'accesso ai dati intile_static
memoria può essere notevolmente più veloce rispetto all'accesso ai dati nello spazio globale (array
oarray_view
oggetti ). Viene creata un'istanza di unatile_static
variabile per ogni riquadro e tutti i thread nel riquadro hanno accesso alla variabile. In un tipico algoritmo affiancato, i dati vengono copiati intile_static
memoria una volta dalla memoria globale e quindi a cui si accede molte volte dallatile_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 atile_barrier::wait
. Non è possibile garantire l'ordine in cui verranno eseguiti i thread, ma solo che nessun thread nel riquadro eseguirà oltre la chiamata atile_barrier::wait
finché tutti i thread non hanno raggiunto la chiamata. Ciò significa che usando iltile_barrier::wait
metodo è possibile eseguire attività in base al riquadro anziché a thread per thread. Un tipico algoritmo di collegamento include codice per inizializzare latile_static
memoria per l'intero riquadro seguito da una chiamata atile_barrier::wait
. Il codice seguentetile_barrier::wait
contiene calcoli che richiedono l'accesso a tutti itile_static
valori.Indicizzazione locale e globale. È possibile accedere all'indice del thread rispetto all'intero
array_view
oggetto oarray
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 accederetile_static
alle variabili e l'indicizzazione globale per accedere allearray
variabili earray_view
.classe tiled_extent e classe tiled_index. Si usa un
tiled_extent
oggetto anziché unextent
oggetto nellaparallel_for_each
chiamata. Si usa untiled_index
oggetto anziché unindex
oggetto nellaparallel_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.
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
.
Il vettore delle
Description
strutture viene copiato in un oggetto 8x9array_view
.Il
parallel_for_each
metodo viene chiamato con untiled_extent
oggetto come dominio di calcolo. L'oggettotiled_extent
viene creato chiamando ilextent::tile()
metodo delladescriptions
variabile. I parametri di tipo della chiamata aextent::tile()
,<2,3>
, specificano che vengono creati riquadri 2x3. La matrice 8x9 viene quindi affiancata in 12 riquadri, quattro righe e tre colonne.Il
parallel_for_each
metodo viene chiamato utilizzando untiled_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>()
).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:
RawData viene archiviato in una matrice 8x8.
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. Laarray
classe è una di esse.Le dimensioni della matrice e le dimensioni del campione vengono definite usando
#define
istruzioni , perché i parametri di tipo inarray
,extent
array_view
, etiled_index
devono essere valori costanti. È anche possibile usareconst int static
le dichiarazioni. Come vantaggio aggiuntivo, è semplice modificare le dimensioni del campione per calcolare la media su 4x4 riquadri.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.È 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 atile_barrier::wait
.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 unfor
ciclo.I dati nella
averages
variabile, perché si tratta di unarray
oggetto , devono essere copiati di nuovo nell'host. In questo esempio viene usato l'operatore di conversione vettoriale.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