Présentation de C++ AMP
Le parallélisme massif accéléré par C++ (C++ AMP) accélère l'exécution du code C++ en tirant parti du matériel tel que le processeur graphique (GPU) sur une carte graphique.À l'aide de C++ AMP, vous pouvez coder les algorithmes multidimensionnels de données afin que l'opération puisse être accélérée à l'aide du parallélisme sur du matériel hétérogène.Le modèle de programmation C++ AMP inclut la prise en charge des tableaux multidimensionnels, de l'indexation, du transfert de mémoire, du tiling ,et une bibliothèque de fonctions mathématiques.Vous pouvez utiliser les extensions de langage C++ AMP pour contrôler la façon dont les données sont déplacées de l'UC au GPU et inversement, afin que vous puissiez améliorer les performances.
Configuration requise
Windows 7, Windows 8, Windows Server 2008 R2 ou Windows Server 2012
Niveau 11,0 de fonctionnalité DirectX 11 ou ultérieure matériel
Pour déboguer sur l'émulateur de logiciels, Windows 8 ou Windows Server 2012 est requis.Pour déboguer sur le matériel, vous devez installer les pilotes de votre carte graphique.Pour plus d'informations, consultez Débogage du code GPU.
Introduction
Les deux exemples suivants illustrent les composants principaux de C++AMP.Supposons que vous souhaitez ajouter les éléments correspondants de deux tableaux unidimensionnels.Par exemple, vous souhaitez ajouter {1, 2, 3, 4, 5} et {6, 7, 8, 9, 10} pour obtenir {7, 9, 11, 13, 15}.Sans utiliser C++ AMP, vous pouvez écrire le code suivant pour ajouter les nombres et afficher les résultats.
#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";
}
}
Les parties importantes du code sont les suivantes :
Données : Les données sont constitués de trois tableaux.Tous ont les mêmes rang (un) et la même longueur (cinq).
Itération : La première boucle for fournit un mécanisme pour itérer au sein de les éléments des tableaux.Le code que vous souhaitez exécuter pour calculer les sommes est contenu dans le premier bloc for .
Index : La variable idx accède aux éléments des tableaux.
À l'aide de C++ AMP, vous pouvez écrire le code suivant à la place.
#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";
}
}
Les mêmes éléments de base sont présents, mais les constructions C++ AMP sont utilisées :
Données : Vous utilisez des tableaux C++ pour construire trois objets C++ AMP array_view .Vous fournissez quatre valeurs pour construire un objet array_view : les valeurs de données, le rang, le type d'élément, et la longueur de l'objet array_view dans chaque dimension.Le rang et type sont passés comme paramètres de type.Les données et la longueur sont passées en tant que paramètres de constructeur.Dans cet exemple, le tableau C++ qui est passé au constructeur est unidimensionnel.Le rang et la longueur sont utilisés pour construire la forme rectangulaire des données dans l'objet d' array_view , et les données sont utilisées pour remplir un tableau.La bibliothèque Runtime inclut également array, classe, qui possède une interface qui ressemble à la classe array_view expliquée ultérieurement dans cet article.
Itération : parallel_for_each, fonction (C++ AMP) fournit un mécanisme pour itérer au sein de les éléments de données, ou le domaine de calcul.Dans cet exemple, le champ de calcul est spécifié par sum.extent.Le code que vous souhaitez exécuter est contenu dans une expression lambda, ou fonction kernel.La clause restrict(amp) indique que seul le sous-ensemble du langage C++ que C++ AMP peut accélérer est utilisé.
Index : La variable index, classe , idx, est déclarée avec un rang de un pour correspondre au rang de l'objet array_view .À l'aide de l'index, vous pouvez accéder aux éléments individuels des objets array_view .
La mise en forme et indexation des données : indexer et étendre
Vous devez définir les valeurs de données et déclarer la forme des données avant de pouvoir exécuter le code de noyau.Toutes les données sont définies comme un tableau rectangulaire, et vous pouvez définir n'importe quel rang (nombre de dimensions) pour le tableau.Les données peuvent être de n'importe quelle taille dans n'importe quelle dimension.
index, classe
La index, classe spécifie un emplacement dans l'objet array ou array_view en encapsulant l'offset de l'origine dans chaque dimension dans un objet.Lorsque vous accédez à un emplacement dans le tableau, vous passez un objet index à l'opérateur d'indexation, [], au lieu d'une liste d'index d'entiers.Vous pouvez accéder aux éléments dans chaque dimension à l'aide de array::operator(), opérateur ou array_view::operator(), opérateur.
L'exemple suivant crée un index unidimensionnel qui spécifie le troisième élément dans un objet array_view unidimensionnel .L'index est utilisé pour imprimer le troisième élément dans l'objet array_view .Le résultat est 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'exemple suivant crée un index à deux dimensions qui spécifie l'élément ligne = 1 et colonne = 2 dans un objet array_view à deux dimensions.Le premier paramètre dans le constructeur de index est le composant de ligne, et le second paramètre est le composant de colonne.Le résultat est 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'exemple suivant crée un index à trois dimensions qui spécifie l'élément où la profondeur = 0, la ligne = 1, et la colonne = 3 dans un objet array_view à trois dimensions.Notez que le premier paramètre est le composant de profondeur, le second paramètre est le composant de ligne, et le troisième paramètre est le composant de colonne.Le résultat est 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
extent, classe
La extent, classe (C++ AMP) spécifie la longueur des données dans chaque dimension de l'objet array ou array_view .Vous pouvez créer une étendue et l'utiliser pour créer un objet array ou array_view .Vous pouvez également récupérer l'étendue de array ou d'un objet array_view existant.L'exemple suivant écris la longueur de l'étendue dans chaque dimension d'un objet 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";
L'exemple suivant crée un objet array_view qui a les mêmes dimensions que l'objet dans l'exemple précédent, mais cet exemple utilise un objet extent au lieu d'utiliser des paramètres explicites dans le constructeur d' 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";
Le transfert de données à l'accélérateur : tableau et array_view
Deux conteneurs de données utilisés pour déplacer des données à l'accélérateur sont définis dans la bibliothèque Runtime.Il y a la classe array, classe et la classe array_view, classe.La classe array est une classe conteneur qui crée une copie complète des données lorsque l'objet est construit.La classe array_view est une classe wrapper qui copie les données lorsque la fonction principale accède aux données.Lorsque les données sont nécessaires sur le périphérique source de données sont restaurées.
array, classe
Lorsqu'un objet array est construit, une copie complète des données est créée sur l'accélérateur si vous utilisez un constructeur qui inclut un pointeur vers le groupe de données.La fonction kernel modifie la copie sur l'accélérateur.Lorsque l'exécution de la fonction kernel est terminée, vous devez copier les données dans la structure de données sources.l'exemple suivant multiplie chaque élément dans un vecteur par 10.Une fois la fonction kernel terminée, l' opérateur de conversion vectoriels est utilisé pour copier les données dans l'objet vecteur.
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";
}
array_view, classe
La classe array_view a quasiment les mêmes membres que la classe array , mais le comportement sous-jacent n'est pas identique.Les données passées au constructeur de array_view ne sont pas répliquées dans le GPU comme elles le sont avec un constructeur de array .À la place, les données sont copiées dans l'accélérateur lorsque la fonction kernel est exécutée.Par conséquent, si vous créez deux objets array_view qui utilisent les mêmes données, les deux objets array_view font référence au même espace mémoire.Dans ce cas, vous devez synchroniser tout accès multithread.Le principal avantage d'utiliser la classe array_view est que les données sont déplacées uniquement si nécessaire.
Comparaison de tableau et d'array_view
Le tableau suivant résume les similitudes et les différences entre array et array_view .
Description |
array (classe) |
array_view (classe) |
---|---|---|
Lorsque le rang est déterminé |
Au moment de la compilation. |
Au moment de la compilation. |
Lorsque l'étendue est déterminée |
Au moment de l'exécution |
Au moment de l'exécution |
Forme |
Rectangulaire |
Rectangulaire |
Stockage des données |
Est un conteneur de données. |
Est un wrapper de données. |
Copier |
Copie complète et explicite à la définition. |
Copie implicite lorsqu'il est accédé par la fonction kernel. |
Récupération des données |
En copiant les données du tableau en un objet sur le thread CPU. |
Par l'accès direct de l' array_view ou en appelant array_view::synchronize, méthode pour continuer l'accès aux données dans le conteneur d'origine. |
Exécuter le code sur les données : parallel_for_each
La fonction parallel_for_each définit le code à exécuter sur l'accélérateur par rapport aux données dans l'objet array ou array_view .Prenons le code suivant de l'introduction de cette rubrique.
#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";
}
}
La méthode parallel_for_each prend deux arguments, un domaine de calcul et une expression lambda.
Le Le champ de calcul est un objet extent ou un objet tiled_extent qui définit l'ensemble de threads à créer pour une exécution en parallèle.Un thread est généré pour chaque élément dans le domaine de calcul.Dans ce cas, l'objet extent est unidimensionnel et possède cinq éléments.Par conséquent, cinq threads sont démarrés.
L' L'expression lambda définit le code à exécuter sur chaque thread.La clause de capture, [=] spécifie que le corps de l'expression lambda accède à toutes les variables capturées par valeur, qui sont dans ce cas a, b, et sum.Dans cet exemple, la liste de paramètres crée une variable unidimensionnelle index nommée idx.La valeur de idx[0] est 0 dans le premier thread et est incrémentée d'un dans chaque thread suivant.La clause restrict(amp) indique que seul le sous-ensemble du langage C++ que C++ AMP peut accélérer est utilisé.Les restrictions sur les fonctions qui ont le modificateur de restriction sont décrites dans clause de restriction (C++ ampère).Pour plus d'informations, consultez Syntaxe d'expression lambda.
L'expression lambda peut inclure le code à exécuter ou peut appeler une fonction kernel séparée.La fonction kernel doit inclure le modificateur restrict(amp) .L'exemple suivant équivaut au précédent, mais il appelle une fonction kernel distincte.
#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";
}
}
Accélérer le code : mosaïques et cloisonnements
Vous pouvez gagner de l'accélération supplémentaire à l'aide des mosaïques.Une mosaïque divise les threads dans les sous-ensembles rectangulaires égaux ou tuiles.Vous déterminez la taille appropriée de tuile sur votre groupe de données et l'algorithme que vous codez.Pour chaque thread, vous avez accès à l'emplacement global d'un élément de données par rapport à array entier ou à array_view et l'accès à l'emplacement local par rapport à la tuile.L'utilisation de la valeur d'index local simplifie le code parce que vous n'avez pas à écrire le code pour convertir des valeurs d'index de global à local.Pour utiliser la mosaïque, appeler extent::tile, méthode dans le domaine de calcul dans la méthode parallel_for_each , et utiliser un objet tiled_index dans l'expression lambda.
Dans les applications classiques, les éléments d'une tuile sont associés, le code doit accéder et maintenir des traces des valeurs à travers la tuile.Utilisez le mot-clé mot clé tile_static et tile_barrier::wait, méthode pour ce faire.Une variable avec le mot clé d' tile_static a une portée dans une tuile entière, et une instance de la variable est créée pour chaque tuile.Vous devez gérer la synchronisation de l'accès à la variable du thread de la tuile.La tile_barrier::wait, méthode arrête l'exécution du thread actuel jusqu'à ce que tous les threads de la tuile ont atteint l'appel à tile_barrier::wait.Vous pouvez accumuler des valeurs entre la tuile en utilisant des variables tile_static .Vous pouvez ensuite terminer les calculs qui requièrent l'accès à toutes les valeurs.
Le diagramme suivant représente un tableau à deux dimensions de données d'exemple qui sont organisées en tuile.
l'exemple de code suivant utilise les données d'exemple du diagramme précédent.Le code remplace chaque valeur de la tuile par la moyenne des valeurs de la tuile.
// 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
Bibliothèques mathématiques
C++ AMP inclut deux bibliothèques mathématiques.La bibliothèque double précision dans Concurrency::precise_math, espace de noms fournit la prise en charge des fonctions double précision.Elle fournit également la prise en charge des fonctions simple précision, bien que la prise en charge double précision sur le matériel est toujours requise.Il est conforme à la Spécification C99 (ISO/IEC 9899).L'accélérateur doit prendre en charge la double précision complète.Vous pouvez déterminer si c'est effectué en vérifiant la valeur d' accelerator::supports_double_precision, données membres.La bibliothèque mathématique rapide, dans Concurrency::fast_math, espace de noms, contient un autre ensemble de fonctions mathématiques.Ces fonctions, qui prennent uniquement en charge les opérandes float, s'éxécutent plus rapidement mais ne sont pas aussi précises que celles dans la bibliothèque mathématiques double précision.Les fonctions sont contenues dans le fichier d'en-tête <amp_math.h> et sont toutes déclarées avec restrict(amp).Les fonctions dans le fichier d'en-tête <cmath> sont importées dans les espaces de noms fast_math et precise_math .Le mot clé restrict est utilisé pour faire la distinction entre la version d' <cmath> et la version de C++ AMP. Le code suivant calcule le logarithme de base 10, à l'aide de la méthode rapide, de chaque valeur dans le domaine de calcul.
#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";
}
}
Bibliothèque graphique
C++ AMP inclut une bibliothèque graphique conçue pour la programmation graphique accélérée.Cette bibliothèque est utilisée uniquement sur les appareils qui prennent en charge les fonctionnalités graphiques natives.Les méthodes sont dans Concurrency::graphics, espace de noms et sont contenues dans le fichier d'en-tête <amp_graphics.h> .Les éléments clés de la bibliothèque graphique sont :
texture, classe: Vous pouvez utiliser la classe de texture pour créer des textures depuis la mémoire ou depuis un fichier.Les textures ressemblent aux tableaux car ils contiennent des données, et elles ressemblent aux conteneurs de la Standard Template Library (STL) en ce qui concerne la construction d'assignation et de copie.Pour plus d'informations, consultez Conteneurs STL.Les paramètres de modèle pour la classe texture sont le type d'élément et le rang.Le rang peut être 1, 2, ou 3.Le type d'élément peut être l'un des types vectoriels courts décrits ultérieurement dans cet article.
writeonly_texture_view, classe: Fournit l'accès en écriture seule à une texture.
bibliothèque vectorielle courte: Définit un ensemble de types vectoriels courts de longueur 2, 3 et 4, basés sur int, uint, float, double, standard, ou unorm.
applications d'Windows Store
Comme d'autres bibliothèques C++, vous pouvez utiliser C++ AMP dans vos applications Windows Store .Ces articles décrivent comment inclure du code C++ AMP dans des applications qui est créé à l'aide de C++, du C#, Visual Basic, ou de JavaScript :
Procédure pas-à-pas : Création d'un composant runtime Windows en C++ appelé en JavaScript
Comment utiliser C++ AMP depuis C# à l'aide de les fenêtres d'exécution
C++ AMP et visualiseur de concurrence
Le visualiseur de concurrence inclut la prise en charge de l' d'analyse du code C++ AMP.Ces articles décrivent les fonctionnalités suivantes :
Recommandations de performance
Le modulo et la division des entiers non signés ont de bien meilleures performances que le modulo et la division d'entier signé.Nous vous recommandons d'utiliser des entiers non signés si possible.
Voir aussi
Référence
Autres ressources
C++ AMP (C++ Accelerated Massive Parallelism)
Blog sur la programmation parallèle en code natif (éventuellement en anglais)