Présentation de C++ AMP
C++ AMP (C++ Accelerated Massive Parallelism) 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 des tableaux multidimensionnels, l'indexation, le transfert de mémoire, la mosaïque 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
Fonctionnalité DirectX 11 de niveau 11.0 ou matériel plus récent
Pour effectuer un débogage sur l'émulateur de logiciel, Windows 8 ou Windows Server 2012 est requis. Pour effectuer un débogage 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ées 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 des é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 le type sont passés en tant que 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 array_view, et les données sont utilisées pour remplir le 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 des é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 noyau. 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.
Mise en forme et indexation des données : index et extent
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.
Classe index
index, classe spécifie un emplacement dans l'objet array ou array_view en encapsulant le décalage de l'origine dans chaque dimension au sein d'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 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
Classe extent
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 d'un objet array ou array_view existant. L'exemple suivant écrit 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 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";
Déplacement de données vers l'accélérateur : array 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, les données sont restaurées.
Classe array
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 noyau modifie la copie sur l'accélérateur. Lorsque l'exécution de la fonction noyau 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 noyau 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";
}
Classe array_view
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 noyau 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 les classes array et array_view.
Description |
classe array |
classe array_view |
---|---|---|
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 de 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 en cas d'accès par la fonction noyau. |
Extraction de données |
En copiant les données du tableau dans un objet sur le thread CPU. |
Par l'accès direct de l'objet array_view ou en appelant array_view::synchronize, méthode pour continuer l'accès aux données dans le conteneur d'origine. |
Mémoire partagée avec array et array_view
La mémoire partagée est la mémoire accessible par l'UC et l'accélérateur. L'utilisation de la mémoire partagée élimine ou réduit considérablement la charge mémoire des données de copie entre l'UC et l'accélérateur. Bien que la mémoire soit partagée, elle n'est pas accessible simultanément le processeur et l'accélérateur, sans quoi un comportement aléatoire est à craindre.
Les objets array peuvent être utilisés pour spécifier un contrôle fin sur l'utilisation de la mémoire partagée si l'accélérateur associé la prend en charge. Le fait qu'un accélérateur prenne en charge la mémoire partagée est déterminée par la propriété supports_cpu_shared_memory de l'accélérateur, qui retourne true lorsque la mémoire partagée est prise en charge. Si la mémoire partagée est prise en charge, l'access_type, énumération par défaut pour les allocations de mémoire sur l'accélérateur est déterminé par la propriété default_cpu_access_type. Par défaut, array et les objets array_view prennent le même access_type que le accelerator associé primaire.
En définissant la propriété array::cpu_access_type, membre de données d'un array explicitement, vous pouvez contrôler avec précision la façon dont la mémoire partagée est utilisée, afin d'optimiser l'application en fonction des caractéristiques de performance du matériel, d'après les modèles d'accès mémoire de ses cœurs de calcul. array_view reflète le même cpu_access_type que le array auquel il est associé ; ou si array_view est construit sans source de données, son access_type reflète l'environnement qui l'a entraîné à allouer du stockage. Autrement dit, s'il est d'abord accessible par l'hôte (UC), il se comporte comme s'il avait été créé sur une source de données d'UC et partage l'access_type de l'accelerator_view associé par capture. Toutefois, s'il est d'abord accessible par un accelerator_view, il se comporte comme s'il avait été créé sur un array créé sur cet accelerator_view et partage l'access_type de l'array.
L'exemple de code suivant indique comment déterminer si l'accélérateur par défaut prend en charge la mémoire partagée, puis crée plusieurs tableaux dont les configurations cpu_access_type sont différentes.
#include <amp.h>
#include <iostream>
using namespace Concurrency;
int main()
{
accelerator acc = accelerator(accelerator::default_accelerator);
// Early out if the default accelerator doesn’t support shared memory.
if (!acc.supports_cpu_shared_memory)
{
std::cout << "The default accelerator does not support shared memory" << std::endl;
return 1;
}
// Override the default CPU access type.
acc.default_cpu_access_type = access_type_read_write
// Create an accelerator_view from the default accelerator. The
// accelerator_view inherits its default_cpu_access_type from acc.
accelerator_view acc_v = acc.default_view;
// Create an extent object to size the arrays.
extent<1> ex(10);
// Input array that can be written on the CPU.
array<int, 1> arr_w(ex, acc_v, access_type_write);
// Output array that can be read on the CPU.
array<int, 1> arr_r(ex, acc_v, access_type_read);
// Read-write array that can be both written to and read from on the CPU.
array<int, 1> arr_rw(ex, acc_v, access_type_read_write);
}
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 domaine 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'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 noyau séparée. La fonction noyau doit inclure le modificateur restrict(amp). L'exemple suivant équivaut au précédent, mais il appelle une fonction noyau 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 en sous-ensembles rectangulaires égaux ou vignettes. Vous déterminez la taille appropriée de la mosaïque dans 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'emplacement local par rapport à la mosaïque. 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 mosaïque sont associés, le code doit accéder et faire le suivi des valeurs dans toute la mosaïque. Utilisez le mot-clé mot clé tile_static et tile_barrier::wait, méthode pour ce faire. Une variable avec le mot clé tile_static a une portée dans une mosaïque entière, et une instance de la variable est créée pour chaque mosaïque. Vous devez gérer la synchronisation de l'accès à la variable du thread de la mosaïque. tile_barrier::wait, méthode arrête l'exécution du thread actuel jusqu'à ce que tous les threads de la mosaïque aient atteint l'appel à tile_barrier::wait. Vous pouvez accumuler des valeurs entre la mosaïque 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 mosaïque.
L'exemple de code suivant utilise les données d'exemple du diagramme précédent. Le code remplace chaque valeur de la mosaïque par la moyenne des valeurs de la mosaïque.
// 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. Elle est conforme à la spécification C99 (ISO/IEC9899). L'accélérateur doit prendre en charge la double précision complète. Vous pouvez déterminer si l'opération est effectuée en vérifiant la valeur de 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'exé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 distinguer la version <cmath> de la version C++ AMP. Le code suivant calcule le logarithme en base 10 de chaque valeur qui se trouve dans le domaine de calcul, en utilisant la méthode rapide.
#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 de graphiques
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 périphériques 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 STL (Standard Template Library) en termes de construction de copie et d'assignation. 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 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 :
C++ AMP et visualiseur de concurrence
Le visualiseur de concurrence inclut la prise en charge pour l'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 des entiers signés. Nous vous recommandons d'utiliser des entiers non signés si possible.