Partager via


Cet article a fait l'objet d'une traduction automatique.

Windows avec C++

Visual C++ 2010 et la bibliothèque de modèles parallèle

Kenny Kerr

Cet article repose sur une version préliminaire de Visual Studio 2010. Toutes les informations sont sujets à modification.

Contenu

Améliorations de langage
Algorithmes parallèles
Tâches et les groupes de tâches

Visual C++ devient une mise à niveau majeure dans la version 2010 de Visual Studio. De nombreuses fonctionnalités nouvelles langue et de bibliothèque sont conçus uniquement pour rendre plus facile et plus naturel pour exprimer vos désirs dans le code. Mais comme a toujours été le cas avec C++, la combinaison de ces fonctionnalités est ce qui rend C++ tel une langue puissante et expressif.

Donc ce mois-ci je vais présenter des ajouts à la langue C++ qui Visual C++ a ajouté dans le prochain C ++ 0 x standard. J'AI ensuite examinerons le Bibliothèque de modèles parallèle (PPL) que Microsoft a développé sur et au-dessus de la lettre C ++ 0 x standard pour présenter le parallélisme à vos applications d'une manière qui naturellement complète la bibliothèque C++ standard.

Améliorations de langage

Dans l'article de mai 2008 » Plus de C++: renforcer des applications avec Visual C++ 2008 Feature Pack de Windows« J'AI présenté les ajouts à la bibliothèque C++ standard comme partie technique rapport 1 (TR1) qui a été introduit à l'origine avec le Pack de déploiement de Visual C++ 2008 et est maintenant inclus avec Visual Studio 2008 SP1. Dans cet article, J'AI illustré la prise en charge des objets de fonction au moyen de la classe de modèle de fonction et la fonction de modèle de liaison. Possibilité de traiter les fonctions polymorphically résolu grand nombre de problèmes difficile les développeurs C++ souvent rencontrés lorsque écrire ou à l'aide des algorithmes génériques.

Comme un récapitulatif, Voici un exemple de comment vous initialiser un objet de fonction avec la norme plus l'algorithme :

function<int (int, int)> f = plus<int>();
int result = f(4, 5);

À l'aide de la fonction de liaison, vous pouvez transformer une fonction qui fournit la fonctionnalité nécessaire mais n'a pas assez la signature droite.

Dans l'exemple suivant, j'utilise la fonction de liaison avec des espaces réservés pour initialiser l'objet de fonction avec une fonction membre :

struct Adder
{
   int Add(int x, int y, void* /*reserved*/)
   {
       return x + y;
   }
};

Adder adder;
function<int (int, int)> f = bind(&Adder::Add, &adder, _1, _2, 
    static_cast<void*>(0));
int result = f(4, 5);

Il existe deux problèmes surviennent avec l'utilisation de ces ajouts de bibliothèque ne peut pas être facilement surmonter sans améliorations du langage. Pour commencer, il est souvent inefficace définir explicitement un objet de fonction, qu'il ajoute certain surcharge le compilateur peut sinon avoir évitée. Il peut également être très redondante et fastidieux de re-declare le prototype de fonction lorsque le compilateur sait clairement la signature qui correspond mieux à l'expression d'initialisation.

C'est dans lequel le nouveau mot-clé automatique vous aide. Il peut servir de lieu de définir explicitement le type d'une variable, ce qui est utile dans modèle metaprogramming où les types spécifiques sont difficiles à définir ou complexes exprimer. Voici à quoi il ressemble :

auto f = plus<int>();

Les définitions de fonctions eux-mêmes peuvent également bénéficier d'une amélioration. Vous pouvez réutiliser souvent des algorithmes utiles, telles que celles dans la bibliothèque C++ standard. Plus souvent à pas, toutefois, vous devez écrire une fonction propres au domaine à un usage très spécifique qui n'est pas généralement réutilisable.

Mais étant donné que la fonction doit être définie ailleurs, vous devez envisager conception logique et physique. Ne serait-il pas agréable si la définition peut être insérée dans l'emplacement de tout cela est nécessaire, simplifier la compréhension du code en améliorant la localité de la logique et améliorer l'encapsulation de la conception globale ? L'ajout d'expressions lambda exactement qui permet de :

auto f = [](int x, int y) { return x + y; };

Les expressions lambda définissent des objets sans nom fonction, parfois appelés fermetures. La [] est l'indicateur indique au compilateur qu'une expression lambda commence. On parle le introducer lambda, et qu'elle est suivie par une liste de paramètres. Cette déclaration de paramètre peut également inclure un type de retour, bien qu'il est souvent omis lorsque le compilateur peut déduire le type sans ambiguïté, comme c'est le cas dans l'extrait de code précédent. En fait, la liste de paramètres peut être omise si la fonction doit n'accepter aucun paramètre. L'expression lambda se termine par n'importe quel nombre d'instructions C++ dans accolades.

Les expressions lambda peuvent également capturer des variables pour les utiliser dans la fonction de l'étendue dans laquelle l'expression lambda est définie. Voici un exemple qui utilise une expression lambda pour calculer la somme des valeurs dans un conteneur :

int sum = 0;
for_each(values.begin(), values.end(), [&sum](int value)
{
    sum += value;
});

Accordé, cela pourrait ont été effectuée plus brièvement avec la fonction accumulate, mais qui est manquant du point. Cet exemple montre comment la variable sum est capturée par référence et utilisée dans la fonction.

Algorithmes parallèles

Le PPL introduit un ensemble de constructions de parallélisme et orientés tâche ainsi qu'un nombre d'algorithmes parallèles semblables à ce qui est disponible avec OpenMP dès aujourd'hui. Les algorithmes PPL sont, toutefois, écrit avec modèles C++ au lieu de directives pragma et par conséquent, sont beaucoup plus expressif et flexible. Toutefois, la PPL est fondamentalement différent de OpenMP dans le sens que le PPL promeut un ensemble de primitives et les algorithmes qui sont plus composable et réutilisable comme un ensemble de modèles. En outre, OpenMP est par nature plus déclarative et explicite de questions telles que la planification et, au final, n'est pas partie C++ approprié. Le PPL également repose en haut de l'exécution d'accès concurrentiel, permettant d'interopérabilité potentielle supérieure avec les autres bibliothèques basés sur le même runtime. Nous allons examiner les algorithmes PPL et ensuite voir comment vous pouvez utilisent la fonctionnalité sous-jacente directement pour le parallélisme et orientés tâche.

Dans l'article octobre 2008 de cette colonne " Exploration des algorithmes de haute performance"), J'AI démontré l'avantage d'algorithmes efficaces et les effets de localité et conceptions soucieux de cache sur les performances. J'AI montré comment un algorithme inefficace, single-threaded pour convertir une image de grande taille en nuances de gris a pris 46 secondes, pendant une implémentation efficace dans toujours uniquement un seul thread a pris seulement 2 secondes. Avec un peu sprinkling de OpenMP I a pu paralléliser l'algorithme sur l'axe des Y et réduire le temps encore davantage. la figure 1 affiche le code de l'algorithme OpenMP.

La figure 1 en nuances de gris algorithme à l'aide de OpenMP

struct Pixel
{
    BYTE Blue;
    BYTE Green;
    BYTE Red;
    BYTE Alpha;
};

void MakeGrayscale(Pixel& pixel)
{
    const BYTE scale = static_cast<BYTE>(0.30 * pixel.Red +
                                         0.59 * pixel.Green +
                                         0.11 * pixel.Blue);

    pixel.Red = scale;
    pixel.Green = scale;
    pixel.Blue = scale;
}

void MakeGrayscale(BYTE* bitmap,
                   const int width,
                   const int height,
                   const int stride)
{
    #pragma omp parallel for
    for (int y = 0; y < height; ++y)
    {
        for (int x = 0; x < width; ++x)
        {
            const int offset = x * sizeof(Pixel) + y * stride;

            Pixel& pixel = *reinterpret_cast<Pixel*>(bitmap + offset);

            MakeGrayscale(pixel);
        }
    }
}

Le PPL inclut un parallèle pour les algorithmes qui peuvent très naturellement, avec un peu d'aide à partir d'expressions lambda, remplacer l'utilisation de OpenMP dans la fonction MakeGrayscale à partir de La figure 1 :

parallel_for(0, height, [&] (int y)
{
    for (int x = 0; x < width; ++x)
    {
        // omitted for brevity
    }
});

Comme vous pouvez le voir, la de la boucle, ainsi que le pragma OpenMP ont été remplacé par la fonction parallel_for. Tout d'abord deux paramètres de la fonction définir la plage d'itération, comme pour la précédente pour boucle. Contrairement à OpenMP, qui place des restrictions épais sur la directive parallel_for est une fonction modèle afin de vous le pouvez, par exemple, effectuer une itération sur les types non signés ou itérateurs même complexes dans les conteneurs standard. Le dernier paramètre est un objet fonction qui je définir comme une expression lambda.

Vous remarquerez que le introducer lambda inclut uniquement une esperluette sans explicitement déclarer les variables à capturer. Ceci indique au compilateur de capturer toutes les variables possibles par référence. Puisque les instructions dans l'expression lambda utilisent un nombre de variables, J'AI utilisé ceci comme une abréviation. Veillez, toutefois, car le compilateur ne peut pas optimiser rangement toutes les variables non utilisés, entraînant runtime faibles performances. J'AI pourrait ont explicitement capturées les variables que J'AVAIS besoin avec la liste capture suivante :

 [&bitmap, width, stride]

Comme la parallel_for fonction constitue une alternative parallèle à le pour boucle fournissant itération parallèle sur une plage d'indices, le PPL fournit également la fonction de modèle parallel_for_each si vous ne pouvez parallèle à la fonction for_each standard. Il fournit itération parallèle sur une plage de éléments définis par une paire d'itérateurs, tels que ceux fournis par les conteneurs standard. Bien qu'il effectué plus pertinents pour l'exemple précédent utiliser la fonction parallel_for avec index explicite, il est souvent plus naturelle itérateurs permet de définir une plage d'éléments. Donnée de matrice de nombres, vous pouvez carré leurs valeurs à l'aide de la fonction parallel_for comme suit :

array<int, 5> values = { 1, 2, 3, 4, 5 };

parallel_for(0U, values.size(), [&values] (size_t i)
{
    values[i] *= 2;
});

Mais cette approche est trop longue, exige le tableau pour être capturées par l'expression lambda et, selon le type de conteneur, peut être inefficace. La fonction parallel_for_each résout ces problèmes bien :

parallel_for_each(values.begin(), values.end(), [] (int& value)
{
    value *= 2;
});

Si vous souhaitez simplement exécuter un certain nombre de fonctions en parallèle ou que le parallèle comme est possible en fonction du nombre de threads matériels disponibles, vous pouvez utiliser la fonction de modèle parallel_invoke. Il existe des surcharges disponibles pour accepter n'importe où à partir de 2 à 10 objets fonction. Voici un exemple d'exécuter des fonctions lambda 3 en parallèle :

combinable<int> sum;

parallel_invoke([&] { sum.local() += 1; },
                [&] { sum.local() += 2; },
                [&] { sum.local() += 3; });

int result = sum.combine([] (int left, int right)
{
    return left + right;
});

ASSERT(6 == result);

Cet exemple illustre également une autre classe d'assistance fournie par le PPL. La classe combinable permet très facilement combiner les résultats d'un certain nombre de tâches parallèles avec un minimum de verrouillage. En fournissant une copie locale de la valeur pour chaque thread et puis uniquement combine les résultats de chaque thread une fois le travail parallèle a terminé, la classe combinable évite partie le verrouillage se produit normalement dans ce cas.

Tâches et les groupes de tâches

Le parallélisme dans les algorithmes que j'ai expliqué réel est obtenue au moyen d'une simple API et orientés tâches que vous êtes libre d'utiliser directement. Tâches sont définies avec la classe task_handle initialisée avec un objet de fonction. Tâches sont regroupées et avec une classe task_group qui exécute les tâches et attend qu'effectuer. Bien entendu, le task_group fournit des surcharges utiles, afin que dans de nombreux cas avoir n'est pas encore définir les objets task_handle vous-même et permet l'objet task_group allouer et gérer sa durée de vie pour vous. Voici un exemple d'utilisation d'un task_group pour remplacer la fonction parallel_invoke à partir de l'exemple précédent :

task_group group;
group.run([&] { sum.local() += 1; });
group.run([&] { sum.local() += 2; });
group.run([&] { sum.local() += 3; });
group.wait();

En outre les API que j'ai abordés ici et algorithmes, autres algorithmes parallèles et les classes d'assistance peuvent également être incluses lorsque la bibliothèque de modèles parallèle est enfin lancée avec Visual C++ 2010. Pour maintenir à jour sur les derniers simultanéité d'accès aux données, visitez Parallèle de programmation dans le code natif.

Veuillez envoyer vos questions et commentaires à mmwincpp@microsoft.com.

Kenny Kerr est un artisan logiciel spécialisé dans le développement de logiciels pour Windows. Il a une passion pour écrire et il enseigne aux développeurs sur la conception programmation et de logiciels. Kenny à weblogs.asp. NET/kennykerr.