Cenni preliminari su C++ AMP
Accelerated Massive Parallelism C++ (AMP C++) accelera l'esecuzione del codice C++ sfruttando hardware parallelo, come un'unità di elaborazione grafica (GPU) in una scheda grafica discreta.Tramite AMP C++, è possibile codificare algoritmi di dati multidimensionali in modo che sia possibile accelerare l'esecuzione tramite il parallelismo su hardware eterogeneo.Il modello di programmazione AMP C++ supporta matrici multidimensionali, indicizzazione, trasferimento di memoria, sezionamento e una libreria di funzioni matematiche.È possibile utilizzare le estensioni del linguaggio AMP C++ per controllare il modo in cui i dati vengono spostati dalla CPU alla GPU e viceversa, per migliorare le prestazioni.
Requisiti di sistema
Windows 7, Windows 8, Windows Server 2008 R2 o Windows Server 2012
Livello 11,0 della funzionalità di DirectX 11 o successiva hardware
Per eseguire il debug sull'emulatore del software, è richiesto Windows 8 o Windows Server 2012.Per eseguire il debug su hardware, è necessario installare i driver per la scheda grafica.Per ulteriori informazioni, vedere Debug del codice GPU.
Introduzione
I due esempi seguenti illustrano i componenti principali di AMP C++.Si supponga di voler sommare gli elementi corrispondenti di due matrici unidimensionali.Ad esempio, si vogliono sommare {1, 2, 3, 4, 5} e {6, 7, 8, 9, 10} per ottenere {7, 9, 11, 13, 15}.Senza l'utilizzo di AMP C++, è possibile scrivere il codice seguente per aggiungere i numeri e visualizzare i risultati.
#include <iostream>
void StandardMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5];
for (int idx = 0; idx < 5; idx++)
{
sumCPP[idx] = aCPP[idx] + bCPP[idx];
}
for (int idx = 0; idx < 5; idx++)
{
std::cout << sumCPP[idx] << "\n";
}
}
Le parti principali del codice sono:
Dati: I dati sono costituiti da tre matrici.Tutti hanno lo stesso numero di dimensioni (uno) e lunghezza (cinque).
Iterazione: Il primo ciclo for fornisce un meccanismo per la scorrere gli elementi nelle matrici.Il codice da eseguire per calcolare le somme è contenuto nel primo blocco for.
Indice: La variabile idx accede ai singoli elementi delle matrici.
Mediante AMP C++, è invece possibile scrivere il codice seguente.
#include <amp.h>
#include <iostream>
using namespace concurrency;
const int size = 5;
void CppAmpMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[size];
// Create C++ AMP objects.
array_view<const int, 1> a(size, aCPP);
array_view<const int, 1> b(size, bCPP);
array_view<int, 1> sum(size, sumCPP);
sum.discard_data();
parallel_for_each(
// Define the compute domain, which is the set of threads that are created.
sum.extent,
// Define the code to run on each thread on the accelerator.
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);
// Print the results. The expected output is "7, 9, 11, 13, 15".
for (int i = 0; i < size; i++) {
std::cout << sum[i] << "\n";
}
}
Gli stessi elementi base sono presenti, ma vengono utilizzati i costrutti AMP C++:
Dati: Si utilizzano matrici C++ per creare tre oggetti AMP C++ array_view.Vengono forniti quattro valori per creare un oggetto array_view : i valori dei dati, il numero di dimensioni, il tipo di elemento e la lunghezza dell'oggetto array_view in ciascuna dimensione.Il numero di dimensioni e tipo vengono passati come parametri di tipo.I dati e la lunghezza vengono passati come parametri del costruttore.In questo esempio, la matrice C++ passata al costruttore è unidimensionale.Il numero di dimensioni e lunghezza vengono utilizzati per costruire la forma rettangolare dei dati nell'oggetto array_view e i valori dei dati vengono utilizzati per riempire la matrice.La libreria di runtime include inoltre l' Classe array, che dispone di un'interfaccia simile alla classe array_view e viene discussa più avanti in questo articolo.
Iterazione: Funzione parallel_for_each (C++ AMP) fornisce un meccanismo per scorrere i dati, o il dominio di calcolo.In questo esempio, il dominio di calcolo è specificato da sum.extent.Il codice da eseguire è contenuto in un'espressione lambda, o funzione del kernel.restrict(amp) indica che viene utilizzato solo un sottoinsieme del linguaggio C++ che AMP C++ può velocizzare.
Indice: La variabile Classe index, idx, è dichiarata con un numero di dimensioni pari a uno per adattarsi al numero di dimensioni dell'oggetto array_view.Tramite l'indice, è possibile accedere ai singoli elementi degli oggetti array_view.
Definizione e indicizzazione dei dati: indice ed extent
È necessario definire valori dei dati e dichiararne la forma prima di eseguire il codice kernel.Tutti i dati vengono definiti come una matrice (rettangolare), ed è possibile definire la matrice per avere qualsiasi numero di dimensioni.I dati possono essere di qualsiasi dimensione in una qualsiasi delle dimensioni.
Classe index
La Classe index specifica una posizione nell'oggetto array_view o array incapsulando l'offset dall'origine in ciascuna dimensione in un oggetto.Quando si accede a una posizione nella matrice, si passa un oggetto index all'operatore di indicizzazione, [], anziché un elenco di indici Integer.È possibile accedere agli elementi in ogni dimensione utilizzando Operatore array::operator() o Operatore array_view::operator().
L'esempio seguente consente di creare un indice unidimensionale che specifica il terzo elemento in un oggetto unidimensionale array_view.L'indice viene utilizzato per stampare il terzo elemento nell'oggetto array_view.L'output è 3.
int aCPP[] = {1, 2, 3, 4, 5};
array_view<int, 1> a(5, aCPP);
index<1> idx(2);
std::cout << a[idx] << "\n";
// Output: 3
L'esempio seguente consente di creare un indice bidimensionale che specifica l'elemento in cui la riga = 1 e la colonna = 2 in un oggetto bidimensionale array_view.Il primo parametro nel costruttore index è la componente riga e il secondo parametro è la componente colonna.L'output è 6.
int aCPP[] = {1, 2, 3,
4, 5, 6};
array_view<int, 2> a(2, 3, aCPP);
index<2> idx(1, 2);
std::cout << a[idx] << "\n";
// Output: 6
L'esempio seguente consente di creare un indice tridimensionale che specifica l'elemento in cui la profondità = 0, la riga = 1 e la colonna = 3 in un oggetto tridimensionale array_view.Si noti che il primo parametro è la componente profondità, il secondo parametro è la componente riga e il terzo parametro è la componente colonna.L'output è 8.
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
array_view<int, 3> a(2, 3, 4, aCPP);
// Specifies the element at 3, 1, 0.
index<3> idx(0, 1, 3);
std::cout << a[idx] << "\n";
// Output: 8
Classe extent
La Classe extent (C++ AMP) specifica la lunghezza dei dati in ciascuna dimensione dell'oggetto array_view o array.È possibile creare un extent e utilizzarlo per creare un oggetto array_view o array.È inoltre possibile recuperare l'ambito di un oggetto array o array_view esistente.Nell'esempio seguente viene stampato la lunghezza dell'extent in ogni dimensione di un oggetto array_view.
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// There are 3 rows and 4 columns, and the depth is two.
array_view<int, 3> a(2, 3, 4, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0]<< "\n";
std::cout << "Length in most significant dimension is " << a.extent[0] << "\n";
Nell'esempio seguente viene creato un oggetto array_view con le stesse dimensioni dell'oggetto nell'esempio precedente, ma questo esempio utilizza un oggetto extent anziché utilizzare i parametri espliciti nel costruttore array_view.
int aCPP[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
extent<3> e(2, 3, 4);
array_view<int, 3> a(e, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0] << "\n";
Spostando i dati all'acceleratore: array e array_view
Due contenitori di dati utilizzati per spostare i dati nell'acceleratore sono definiti nella libreria di runtime.Essi sono Classe array e Classe array_view.La classe array è una classe contenitore che crea una copia completa dei dati quando l'oggetto viene costruito.La classe array_view è una classe wrapper che copia i dati quando la funzione del kernel accede ai dati stessi.Quando i dati sono necessari nel dispositivo di origine, i dati vengono copiati indietro.
Classe array
Quando un oggetto array viene costruito, una copia completa dei dati viene creata sull'accelerator se si utilizza un costruttore che include un puntatore al dataset.La funzione del kernel modifica la copia sull'accelerator.Quando l'esecuzione della funzione del kernel è terminata, è necessario copiare i dati alla struttura dati di origine.Nell'esempio seguente si moltiplica per 10 ciascun elemento in un vettore.Dopo che la funzione del kernel è terminata, l' operatore di conversione vettoriale viene utilizzato per copiare i dati nell'oggetto vettoriale.
std::vector<int> data(5);
for (int count = 0; count < 5; count++)
{
data[count] = count;
}
array<int, 1> a(5, data.begin(), data.end());
parallel_for_each(
a.extent,
[=, &a](index<1> idx) restrict(amp)
{
a[idx] = a[idx] * 10;
}
);
data = a;
for (int i = 0; i < 5; i++)
{
std::cout << data[i] << "\n";
}
Classe array_view
L' array_view ha i membri pressoché uguali alla classe array, ma il comportamento è differente.I dati passati al costruttore array_view non vengono replicati nella GPU come invece avviene con il costruttore array.Invece, i dati vengono copiati nell'accelerator quando la funzione del kernel viene eseguita.Pertanto, se si creano due oggetti array_view che utilizzano gli stessi dati, entrambi gli oggetti array_view riferiscono allo stesso spazio di memoria.In questo caso, è necessario sincronizzare qualsiasi accesso multithreading.Il vantaggio principale dell'utilizzo della classe array_view è che i dati vengono spostati solo se necessario.
Confronto tra array e array_view
Nella tabella seguente vengono riepilogate le analogie e le differenze tra le classi array e array_view.
Descrizione |
array (classe) |
array_view (classe) |
---|---|---|
Quando il numero di dimensioni è determinato |
In fase di compilazione. |
In fase di compilazione. |
Quando l'extent viene determinato |
In fase di esecuzione. |
In fase di esecuzione. |
Forma |
Rettangolare. |
Rettangolare. |
Archivio dati |
è un contenitore di dati. |
è un wrapper di dati. |
Copia |
Esplicita e copia completa al momento della definizione. |
Copia implicita quando vi si accede dalla funzione del kernel. |
Recupero dei dati |
Copiando i dati della matrice su un oggetto nel thread della CPU. |
Dall'accesso diretto all'oggetto array_view o chiamando Metodo array_view::synchronize per continuare ad accedere ai dati nel contenitore originale. |
Codice in esecuzione sui dati: parallel_for_each
La funzione parallel_for_each definisce il codice da eseguire nell'accelerator sui dati negli oggetti array_view o array.Si consideri il seguente codice dell'introduzione di questo argomento.
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddArrays() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
Il metodo parallel_for_each accetta due argomenti, il dominio di calcolo e un'espressione lambda.
Il dominio di calcolo è un oggetto extent o un oggetto tiled_extent che definisce il set di thread da creare per l'esecuzione parallela.Un thread viene generato per ogni elemento nel dominio di calcolo.In questo caso, l'oggetto extent è unidimensionale e ha cinque elementi.Di conseguenza, vengono avviati cinque thread.
L' espressione lambda definisce il codice da eseguire su ogni thread.La clausola di acquisizione, [=], specifica che il corpo dell'espressione lambda accede a tutte le variabili acquisite per valore, che in questo caso sono a, b e sum.In questo esempio, l'elenco di parametri crea una variabile unidimensionale index denominata idx.Il valore di idx[0] è 0 nel primo thread e aumenta di uno in ciascun thread successivo.restrict(amp) indica che viene utilizzato solo un sottoinsieme del linguaggio C++ che AMP C++ può velocizzare.Le limitazioni delle funzioni che hanno il modificatore restrict vengono descritte in Clausola di restrizione (AMP C++).Per ulteriori informazioni, vedere Sintassi delle espressioni lambda.
L'espressione lambda può includere il codice da eseguire o può chiamare una funzione del kernel separata.La funzione del kernel deve includere il modificatore restrict(amp).L'esempio seguente è equivalente a quello precedente, ma chiama una funzione del kernel separata.
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddElements(index<1> idx, array_view<int, 1> sum, array_view<int, 1> a, array_view<int, 1> b) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
void AddArraysWithFunction() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp)
{
AddElements(idx, sum, a, b);
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
Codice accelerato: sezioni e barriere
È possibile ottenere un'accelerazione aggiuntiva utilizzando il sezionamento.Il sezionamento divide i thread in sottoinsiemi rettangolari uguali o sezioni.Si determina la dimensione corretta della sezione in base al set di dati e all'algoritmo che si sta programmando.Per ogni thread, è possibile accedere al percorso globale di un elemento relativo all'intero array o array_view e accedere al percorso locale rispetto alla sezione.L'utilizzo del valore locale dell'indice semplifica il codice in quanto non è necessario scrivere il codice per convertire i valori dell'indice da globale a locale.Per utilizzare il sezionamento, si chiama Metodo extent::tile nel dominio di calcolo nel metodo parallel_for_each e si utilizza un oggetto tiled_index nell'espressione lambda.
Nelle applicazioni tipiche, gli elementi in una sezione sono correlati in qualche modo, e il codice deve accedere e tenere traccia dei valori nella sezione.Utilizzare le parole chiave parola chiave tile_static e Metodo tile_barrier::wait per tale scopo.Una variabile con la parola chiave tile_static ha un ambito in un'intera sezione, e un'istanza della variabile viene creata per ogni sezione.È necessario gestire la sincronizzazione dell'accesso ai thread di sezione alla variabile.La Metodo tile_barrier::wait interrompe l'esecuzione del thread corrente finché tutti i thread nella sezione non hanno raggiunto la chiamata a tile_barrier::wait.Pertanto è possibile accumulare valori nella sezione utilizzando variabili tile_static.È quindi possibile completare tutti i calcoli che richiedono l'accesso a tutti i valori.
Il diagramma seguente rappresenta una matrice bidimensionale di dati di campionamento disposti in sezioni.
Nell'esempio di codice seguente vengono utilizzati i dati di campionamento dal diagramma precedente.Il codice sostituisce ogni valore nella sezione con la media dei valori nella sezione.
// Sample data:
int sampledata[] = {
2, 2, 9, 7, 1, 4,
4, 4, 8, 8, 3, 4,
1, 5, 1, 2, 5, 2,
6, 8, 3, 2, 7, 2};
// The tiles:
// 2 2 9 7 1 4
// 4 4 8 8 3 4
//
// 1 5 1 2 5 2
// 6 8 3 2 7 2
// Averages:
int averagedata[] = {
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
};
array_view<int, 2> sample(4, 6, sampledata);
array_view<int, 2> average(4, 6, averagedata);
parallel_for_each(
// Create threads for sample.extent and divide the extent into 2 x 2 tiles.
sample.extent.tile<2,2>(),
[=](tiled_index<2,2> idx) restrict(amp)
{
// Create a 2 x 2 array to hold the values in this tile.
tile_static int nums[2][2];
// Copy the values for the tile into the 2 x 2 array.
nums[idx.local[1]][idx.local[0]] = sample[idx.global];
// When all the threads have executed and the 2 x 2 array is complete, find the average.
idx.barrier.wait();
int sum = nums[0][0] + nums[0][1] + nums[1][0] + nums[1][1];
// Copy the average into the array_view.
average[idx.global] = sum / 4;
}
);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 6; j++) {
std::cout << average(i,j) << " ";
}
std::cout << "\n";
}
// Output:
// 3 3 8 8 3 3
// 3 3 8 8 3 3
// 5 5 2 2 4 4
// 5 5 2 2 4 4
Librerie matematiche
AMP C++ include due librerie matematiche.La libreria della precisione doppia in Spazio dei nomi Concurrency::precise_math fornisce il supporto per le funzioni a precisione doppia.Fornisce inoltre il supporto per funzioni a precisione singola, sebbene il supporto alla precisione doppia sull'hardware sia ancora necessario.Corrisponde a C99 specifica (ISO/IEC 9899).L'accelerator deve supportare la precisione doppia completa.È possibile determinarlo controllando il valore Membro dati accelerator::supports_double_precision.La libreria del calcolo veloce, in Spazio dei nomi Concurrency::fast_math, contiene un altro set di funzioni matematiche.Queste funzioni, che supportano solo operandi float, si eseguono più rapidamente ma non sono precise come quelle nella libreria del calcolo a precisione doppia.Le funzioni sono contenute nel file di intestazione <amp_math.h> e sono tutte dichiarate con restrict(amp).Le funzioni nel file di intestazione <cmath> sono incluse sia nello spazio dei nomi fast_math che in precise_math.La parola chiave restrict viene utilizzata per distinguere la versione <cmath> e la versione AMP C++. Il codice seguente calcola il logaritmo in base 10, utilizzando il metodo veloce, di ogni valore che si trova nel dominio di calcolo.
#include <amp.h>
#include <amp_math.h>
#include <iostream>
using namespace concurrency;
void MathExample() {
double numbers[] = { 1.0, 10.0, 60.0, 100.0, 600.0, 1000.0 };
array_view<double, 1> logs(6, numbers);
parallel_for_each(
logs.extent,
[=] (index<1> idx) restrict(amp) {
logs[idx] = concurrency::fast_math::log10(logs[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
Libreria grafica
AMP C++ include una libreria grafica progettata per la programmazione grafica accelerata.Questa raccolta viene utilizzata solo in dispositivi che supportano nativamente le funzionalità grafiche.I metodi sono in Spazio dei nomi Concurrency::graphics e sono contenuti nel file di intestazione <amp_graphics.h>.I componenti principali della libreria grafica sono:
Classe texture: È possibile utilizzare la classe texture per creare trame dalla memoria o da un file.Le trame sono simili alle matrici perché contengono dati, e sono simili ai contenitori nella Standard Template Library (STL) rispetto alla costruzione dell'assegnamento e della copia.Per ulteriori informazioni, vedere Contenitori STL.I parametri del modello per la classe texture sono il tipo di elemento e il numero di dimensioni.Il numero di dimensioni può essere 1, 2, o 3.Il tipo di elemento può essere uno dei tipi short vector che vengono descritti più avanti in questo articolo.
Classe writeonly_texture_view: Fornisce l'accesso in sola scrittura a una trama.
Breve raccolta vettoriale: Definisce un set di tipi vettore short di lunghezza 2, 3 e 4 basati su int, uint, float, double, norma, o unorm.
applicazioni diWindows Store
Come altre librerie di C++, è possibile utilizzare AMP C++ nelle applicazioni Windows Store.Questi articoli descrivono come includere il codice AMP C++ nelle applicazioni create utilizzando C++, C#, Visual Basic, o JavaScript:
AMP C++ e il visualizzatore di concorrenze
Il visualizzatore di concorrenze include il supporto per analizzare le prestazioni del codice AMP C++.Questi articoli descrivono le seguenti funzionalità:
Suggerimenti relativi alle prestazioni
Il modulo e la divisione di interi senza segno hanno prestazioni significativamente migliori che il modulo e la divisione di interi con segno.È consigliabile utilizzare gli interi senza segno quando possibile.
Vedere anche
Riferimenti
Sintassi delle espressioni lambda