Parallélisme avec la beta 1 de Visual Studio 2010 :Mise à l'échelle d'une application C++
|
![]() |
Comprendre l'application originale.
Décomposer et mesurer l'application.
Implémenter le code parallèle.
Empreinte écologique de l'application.
Les nouvelles offres parallèles incluses dans la beta 1 de Visual Studio 2010 sont naturellement très pertinentes dans un contexte où la multiplication des cœurs dans nos ordinateurs complique la mise à l'échelle de nos applications. Dans un article précédent, j'introduisais le sujet du parallélisme avec Visual Studio 2010 (sur la base de la version CTP distribuée durant la PDC08).
Dans cet article nous présenterons une approche permettant de migrer une application séquentielle vers une application parallèle. En effet, il est parfaitement imaginable que de nombreuses entreprises décident de lancer des projets de migration pour adapter leurs codes séquentiels au monde parallèle. Pour illustrer notre propos, nous allons supposer que nous sommes en charge de la migration des applications en mode parallèle. Notre premier projet concerne une petite application Windows écrite en C++, affichant des vignettes photos. C’est cette application que nous souhaitons paralléliser en utilisant l'offre parallèle de Visual Studio 2010.
Remarques
Dans le cadre de la session HPC304 des TechDays 09 où je présentais "La programmation parallèle pour les développeurs C++", j'avais demandé à mon ami Bob Powell de me développer une petite application graphique GDI+ en C++ à l'aide de Visual Studio 2008. C'est donc cette application non parallèle, qui m'a permis d'illustrer la dernière démonstration de ma session. Cependant quelques soucis d'alimentation électrique ne m'avaient pas permis d'expliquer comment le code avait été adapté côté code source. J'ai donc songé reprendre cette application dans le cadre de la sortie de la beta 1 de Visual Studio 2010 afin d'illustrer à la fois la démarche et l'implémentation parallèle.
Vous avez sans doute déjà rencontré des applications reposant sur un multi threading compliqué, dont les performances étaient médiocres même parfois complètement instables. La programmation parallèle est un art difficile dont les pièges sont compliqués et souvent obscures. Contrairement à ces dernières années où les outils et les modèles de programmation étaient réservés à des experts proches de la programmation système, de nouvelles offres à la fois complètes et plus simples (outillages et abstraction) permettent d'aborder la programmation parallèle avec plus de sérénité. Cependant, même avec de beaux outils il n'est pas toujours aisé de paralléliser une application. Par contre, il existe des méthodes permettant de modéliser des applications en mode parallèle. Pour nous guider dans notre démarche de migration, nous établirons une feuille de route sur la base de quatre grandes étapes:
· Comprendre l'application originale
· Décomposer et mesurer l'application
· Structurer l'application
· Implémenter le code parallèle
A travers ces étapes nous aborderons progressivement la mise en parallèle de notre application tout en conservant une démarche très pragmatique.
Il est naturel de penser que tout le code n'est pas ou ne doit pas être parallélisé. Mais comment introduire du parallélisme dans une application séquentielle existante ? C'est sans doute là que réside toute la difficulté d'adaptation d'une application. Face à ce type de situation, nous ne sommes pas à l'abri de découvrir du code très difficile à adapter, comme c'est souvent le cas dans les projets de migration technique. Cependant, indépendamment de l'état du code et de l'architecture, nous devons au préalable comprendre "comment fonctionne" l'application concernée. Si vous ne connaissez pas l'application, il faudra vous renseigner auprès d'une maitrise d'ouvrage, de l'architecte projet ou de l'équipe de développement afin de saisir son fonctionnement général et son architecture sous-jacente. Si vous ne trouvez aucune personne qui connaisse techniquement l'application et que le code vous semble dans un piteux état, la situation n'est pas forcément désespérée. Le projet sera plus long, mais armé d'un solide courage et de bons outils vous finirez par maitriser le code. Je me permet d'annoncer l'arrivée prochaine d'un outil d'analyse de code C++: CppDepend. A l'instar des versions NDepend dédiées à la plateforme .NET (version d'origine développée par Patrick Smacchia), et xDepend dédiée à la plateforme Java (adaptation réalisée par la société OCTO Technology), cette version est comme son nom l'indique dédiée à l'analyse de codes C++. Actuellement cette version pour C++ est encore en phase de développement, mais il est certain qu'elle sera accueillie avec bonheur par de nombreux développeurs travaillant sur des projets C++ conséquents. Naturellement un outil d'analyse permet d’accélérer votre compréhension, mais vous n'échapperez pas aux fameux remaniements de code afin de découpler, réorganiser ("levelizer") les types de données entre eux (dans ce cadre je vous invite à lire, si ce n'est pas déjà fait, l'ouvrage de John Lakos "Large-Scale C++ Software Design").
La fonctionnalité principale de cette application se résume à afficher des vignettes photos récupérées depuis un répertoire scruté récursivement. Dans sa version d'origine cette application est totalement séquentielle et affiche 100 images réparties dans des sous répertoires différents en 35 secondes environ.
|
![]()
|
Si nous explorons le code, nous pouvons relever quelques grandes étapes illustrant le traitement général de l'application. Comme toutes les application Win32, celle-ci contient une méthode de rappel de type WinProc, appelée ici WndProc. Dans notre cas, ce sont les messages provenant des menus ID_SELECT_DIR et ID_MODE_SERIAL qui vont attirer notre attention, sachant que le traitement du message WM_PAINT contient aussi la responsabilité de réceptionner des vignettes photos.
· Le menu ID_SELECT_DIR permet à l'utilisateur de sélectionner le répertoire d'où l'application recherchera des images. L'action est brève et sans intérêt vis à vis de la mise en parallèle de l'application (le temps est largement consommé par l'utilisateur lui-même).
· Le menu ID_MODE_SERIAL permet de lancer le traitement des images. Pour ce faire c'est la méthode load_images de la classe ImageLoader qui réalise le chargement des noms de fichiers des images à traiter.
· Le message WM_PAINT permet de recevoir les images après traitement, c'est à dire des vignettes à afficher dans la fenêtre principale de l'application. C'est la méthode receive_images de la classe ImageLoader qui assure la réception des images traitées.
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case ID_SELECT_DIR:
g_imagesLoader.select_directory(hWnd);
break;
case ID_MODE_SERIAL:
g_nImages = g_imagesLoader.load_images();
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
int imagesLeft;
PhotoSticker* photo;
hdc = BeginPaint(hWnd, &ps);
photo = g_imagesLoader.receive_images(hWnd, imagesLeft));
PaintImages(hWnd, photo);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
DetroyImages(true);
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}Ayant éliminé le cas du traitement du menu ID_SELECT_DIR, nous passons à l'exploration de la méthode load_images.
int ImagesLoader::load_images()
{
TraverseDirectory dir;
dir.Traverse(m_currentDirectory);
m_filenames = dir.GetFilenames();
return m_filenames.size();
}Cette méthode délègue l'essentiel du chargement à la classe TraverseDirectory qui retourne une liste de noms de fichiers images rencontrés. Le code de la méthode Traverse nous montre que celle-ci est récursive (ce qui est souvent le cas lorsque l'on souhaite traverser une hiérarchie de répertoires).
void TraverseDirectory::Traverse(std::wstring directoryPath)
{
wstring searchPath = wstring(directoryPath + wstring(_T("\\*")));
HANDLE FindHandle = INVALID_HANDLE_VALUE;
WIN32_FIND_DATA FindData;
if((FindHandle = ::FindFirstFile(searchPath.c_str(), &FindData))
== INVALID_HANDLE_VALUE)
{
return;
}
do
{
wstring currentFileOrDirectory(directoryPath);
currentFileOrDirectory += wstring(_T("\\"));
currentFileOrDirectory += wstring(FindData.cFileName);
if((FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
== FILE_ATTRIBUTE_DIRECTORY)
{
if(0 != _tcsicmp(FindData.cFileName,_T(".")) &&
0 != _tcsicmp(FindData.cFileName,_T("..")))
{
Traverse(currentFileOrDirectory);
}
}
else
{
if (isValidFile(currentFileOrDirectory))
{
_filenames.push_back(currentFileOrDirectory);
}
}
}
while (FindNextFile(FindHandle, &FindData) != 0);
FindClose(FindHandle);
return ;
}La dernière méthode qui participe au traitement des images est receive_images. Cette méthode nous montre une série d'appels de méthodes décomposant l'essentiel du traitement des images.
Résumons le fonctionnement de l'application : pour chaque message Windows de type WM_PAINT, la méthode receive_images est appelée. Celle-ci dépile, si la liste des noms de fichiers n'est pas vide, un nom de fichier image à traiter. Ce nom de fichier courant est passé à la méthode LoadImage qui retourne une image. Cette image est passée à la méthode TwistImage qui retourne une image déformée. Cette image déformée est ensuite passée à la méthode CreatePhoto qui retourne une image réduite. Enfin, cette image réduite est passée à la méthode CreateSticher qui nous retourne une vignette. Finalement cette vignette est retournée au niveau de traitement de message WM_PAINT pour être afficher à l'écran via la méthode PaintImages.
PhotoSticker* ImagesLoader::receive_images(int& imagesLeft)
{
PhotoSticker* photoSticker = NULL;
if(m_filenames.size() > 0)
{
wstring filename= m_filenames.front();
m_filenames.pop_front();
TypeBitmap bmp = LoadImage(filename);
TypeBitmap bmpTwisted = TwistImage(bmp);
TypeBitmap bmpImage = CreatePhoto(bmpTwisted);
photoSticker = CreateSticker(bmpImage);
}
imagesLeft = m_filenames.size();
return photoSticker;
}
Après cette première exploration du code, nous avons une meilleure idée du fonctionnement de l'application. Nous pouvons décomposer l'application en plusieurs étapes fonctionnelles:
1. Rechercher récursivement tous les fichiers de type image depuis un répertoire présélectionné par l'utilisateur - LoadImages
2. Chargement de l'image en mémoire - LoadImage
3. Déformation de l'image courante en appliquant des convolutions - TwistImage
4. Réduction de taille de l'image - CreatePhoto
5. Mise en forme de l'image pour afficher une vignette - CreateSticker
Après avoir compris l'usage de l'application et ses grandes étapes nous ne savons toujours pas où nous devons introduire du parallélisme. En effet nous ne connaissons pas le temps d'exécution des différentes étapes que nous avons sélectionnées. Au sein de toutes les étapes identifiées nous avons ajouté une sonde afin de comprendre où est dépensé le temps d'exécution en millisecondes. Le répertoire de recherche de fichiers images contient un total de 97 images de tailles variables.
Le graphique ci-dessous représente des temps moyens de traitement pour une image, excepté pour la méthode LoadImages qui traite toutes les images.
|
![]()
|
Les résultats sont sans équivoque. La méthode TwistImage avec 660 ms, est de loin la plus couteuse. Ensuite arrive largement dernière la méthode CreatePhoto avec 10 ms et enfin LoadImage avec 0,33 ms. Les méthodes LoadImages et CreateSticker sont négligeables (temps moyen inférieur à la milliseconde).
Arrivé à ce stade nous comprenons que c'est la méthode TwistImage qui est largement responsable de la durée d'exécution du traitement.
Dans cette étape nous allons introduire les éléments permettant de structurer notre application en reprenant les grandes étapes déterminées précédemment. Dans de nombreux cas et particulièrement lorsque nous sommes débutants dans la mise en place de code parallèle, il est préférable d'introduire du code parallèle en partant au plus haut dans l'architecture du code.
|
![]()
|
Dans un premier temps nous allons répartir nos étapes en agents de hauts niveaux. Par nature un agent permet de confiner l'état d'une étape ou plusieurs étapes en toute quiétude vis à vis du reste de l'application. La communication inter-agents se déroule exclusivement par des canaux de communication orientés messages. En d'autres termes il n'est pas possible de violer l'intégrité d'un agent via des méthodes publiques, car normalement, mise à part le constructeur, toutes les autres méthodes sont inaccessibles de l'extérieur. Les messages sont l'unique moyen de transmettre de l'information entre un agent et le monde extérieur. Cependant il est parfaitement possible d'interconnecter plusieurs agents avec un jeu de messages applicatifs. Lorsque le traitement se résume à l'appel d'une méthode nous pouvons utiliser une structure plus légère que l'agent : un bloc asynchrone. Les blocs peuvent assurer des rôles divers comme la transformation ou le routage de messages ou bien encore réaliser un simple appel de méthode. D'un point de vue général, la composition d'agents et de blocs nous amène à parler de réseaux de communication dont chaque nœud est à la fois découplé (communication par passage de message) et collaboratif vis à vis du traitement parallélisé.
|
![]()
|
Dans un second temps, nous rechercherons dans le code, des opportunités de parallélisme en orientant notre décomposition suivant deux axes : algorithmes et données.
· Algorithmes : nous chercherons à découper les flots d'instructions en tâches pouvant s'exécuter en parallèle. Naturellementl'opportunité de paralléliser en tâches indépendantes sera soumise aux dépendances éventuelles entre les tâches.
· Données : nous porterons notre attention sur les données réclamées par les tâches et la façon dont elles peuvent êtres décomposées en éléments indépendants. Il est essentiel de garder à l'esprit que ce découpage en données n'a de sens que si les données possèdent entre elles une autonomie et une indépendance de bonne facture. En d'autres termes si les données doivent être fortement synchronisées pour être parallélisées, l'efficacité de l'ensemble sera sans doute médiocre. Parfois, un peu de remaniement de code peut améliorer grandement la mise en parallèle.
Notons que les deux démarches sont fortement complémentaires car la décomposition en tâches implique généralement une décomposition des données. L'idée de distinguer les deux démarches est de favoriser la compréhension et le design de l'architecture parallèle applicative afin de faciliter l'étape d'implémentation.
|
![]()
|
Visual Studio 2010 arrive avec une nouvelle offre parallèle complète comprenant de nouveaux modèles de programmation et accompagnée par un outillage adapté au développement parallèle. Que vous soyez développeur .NET ou C++, vous trouverez tous les éléments pour développer plus facilement vos applications parallèles. Dans notre cas c'est l'offre C++ qui nous permettra d'adapter notre code. Si vous n'avez pas assisté à la session des TechDays "La programmation parallèle pour les développeurs C++" je vous invite à regarder la vidéo ici. Et si vous souhaitez une introduction détaillée du moteur d'exécution parallèle je vous inviteà lire ce billet.
Après avoir bien réfléchi, analysez le code pour trouver des opportunités de parallélisation. Il est temps de passer au code. Je vous invite à visiter du code modifié en reprenant le fil depuis le point d'entrée du programme. Dans la partie principale (WinMain) nous introduisons nos deux nouveaux agents, TraverseAgent et ImageAgent. Puis nous établissons les interconnexions entre les agents.
TraverseAgent traverseAgent;
ImagesAgent imagesAgent;
// setup the network
g_imagesLoader.set_inputFilesFound(traverseAgent.get_outputFilesFound());
traverseAgent.set_inputDirectoryName(g_imagesLoader.get_outputDirectoryName());
imagesAgent.set_inputFileName(traverseAgent.get_outputFileName());
imagesAgent.setup_network(g_imagesLoader.get_inputBitmapTwisted());
imagesAgent.start();
traverseAgent.start();
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// stop agents
send(traverseAgent.get_outputFileName(), ImageMsg(STAT_DONE));
send(g_imagesLoader.get_outputDirectoryName(), ImageMsg(STAT_DONE));Lorsque l'application se termine, on notifie nos agents de manière applicative afin qu'ils se terminent proprement à leur tour.
Dans la version parallélisée du chargement des images, on ne charge pas vraiment les images, mais on envoie le nom du répertoire contenant les images à afficher à l'agent TraverseAgent, puis on attend en retour le nombre d'images trouvées.
intImagesLoader::load_imagesParallel()
{
send(m_outputDirectoryName, ImageMsg(m_currentDirectory));
ImageMsg msg = receive(m_inputFilesFound);
return msg._integer;
}Nous pourrions reprocher à ce code de prévoir une attente qui si elle se révélait longue pénaliserait l'application sur ses besoins de traiter des messages Windows. Mais nos mesures précédentes nous ont montré que la recherche des images sur le disque n'était pas coûteuse. De plus la partie graphique n'est pas en attente active sur les images à afficher.
Dans le code de l'agent, on récupère le nom du répertoire puis on appelle, comme dans le code séquentiel, la classe TraverseDirectory, cependant cette fois nous appelons la méthode TraverseParallel.
void TraverseAgent::run()
{
bool stop = false;
while(!stop)
{
ImageMsg& msg = receive(*m_inputDirectoryName);
switch (msg._status)
{
case STAT_DONE:
stop = true;
break;
default:
if (msg._type == MSG_STRING)
{
TraverseDirectory dir(&m_outputFileName);
dir.TraverseParallel(msg._filenameOrDirectory);
send(m_outputFilesFound, ImageMsg(dir.GetCount()));
}
}
}
}Une fois le répertoire visité, nous retournons à la méthode load_imagesParallel le nombre d'images trouvées.
La méthode TraverseParallel ne contient pas vraiment de code parallèle, car les mesures nous ont montré que ce traitement était rapide.
voidTraverseDirectory::TraverseParallel(std::wstring directoryPath)
{
wstring searchPath = wstring(directoryPath + wstring(_T("\\*")));
HANDLE FindHandle = INVALID_HANDLE_VALUE;
WIN32_FIND_DATA FindData;
if(INVALID_HANDLE_VALUE
== (FindHandle = ::FindFirstFile(searchPath.c_str(), &FindData)))
{
return;
}
do
{
wstring currentFileOrDirectory(directoryPath);
currentFileOrDirectory += wstring(_T("\\"));
currentFileOrDirectory += wstring(FindData.cFileName);
if((FindData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
== FILE_ATTRIBUTE_DIRECTORY)
{
if(0 != _tcsicmp(FindData.cFileName,_T("."))
&& 0 != _tcsicmp(FindData.cFileName,_T("..")))
{
TraverseParallel(currentFileOrDirectory);
}
}
else
{
if (isValidFile(currentFileOrDirectory))
{
send(_output, ImageMsg(currentFileOrDirectory));
_count ++;
}
}
}
while (FindNextFile(FindHandle, &FindData) != 0);
FindClose(FindHandle);
return;
}Contrairement à la version séquentielle, ici on n'insère pas les noms de fichiers dans une liste STL, mais on les repousse vers l'agent ImagesAgent. En procèdant ainsi, nous favorisons le débit du flux des traitements des images trouvées.
Le code suivant, concerne les parties les plus parallélisées de l'application. En effet le chargement LoadImages et la déformation TwistImages des images sont des traitements clefs. Dans notre structuration applicative, nous les aurions sans doute introduits dans des agents différents, mais pour des raisons de simplicité, je les ai rassemblés au sein d'une seule méthode.
void ImagesAgent::run()
{
bool stop = false;
while (!stop)
{
ImageMsg msg;
if (try_receive(m_inputFileName, msg))
{
stop = insert_msg(msg);
}
else
{
list<TypeBitmap> images;
LoadImages(images);
TwistImages(images);
stop = insert_msg(receive(m_inputFileName));
}
}
}On peut noter l'usage de la version asynchrone de la méthode receive, try_receive. Pour traiter en masse les images, on accumule les noms des fichiers à charger dans une liste STL (méthode insert_msg). S’il n'y a pas de message à recevoir, le code charge massivement les images, LoadImages. Les images chargées sont accumulées dans une liste d'images qui est ensuite traitée par la méthode TwistImages. Enfin, après avoir traité tous les éléments en attente, on attend cette fois-ci via la méthode receive le prochain nom de fichier à traiter.
voidImagesAgent::LoadImages(list<TypeBitmap>& images)
{
if (m_filenames.size())
{
combinable<list<TypeBitmap>> combinableImageList;
parallel_for_each(m_filenames.begin(), m_filenames.end(), [&](std::wstring filename)
{
combinableImageList.local().push_back(LoadImage(filename));
});
combinableImageList.combine_each([&](list<TypeBitmap>& localList)
{
images.splice(images.begin(), localList);
});
m_filenames.clear();
}
}Dans cette méthode on parallélise le chargement des fichiers via un conteneur STL. Nous disposons dans la librairie PPL, parallel_for_each permettant de traiter en parallèle des conteneurs STL. On note la déclaration de variable combinableImageList qui est une instance du type templatecombinable paramétrée avec une liste STL de type TypeBitmap. La motivation de la classe combinable est de simplifier l'écriture du code de synchronisation lorsque nous partageons comme ici un conteneur STL entre plusieurs tâches. Ainsi notre liste STL est capable d'être plongée dans plusieurs tâches s'exécutant en parallèle.
On note l'usage de la méthode parallel_for_each en troisième argument d’une lambda expression, facilement reconnaissable par son prologue []. Si vous souhaitez allez un peu plus loin sur les lambdas expressions du C++0X, je vous invite à lire ce billet. Le corps de notre lambda fait appel à notre variable combinableImageList qui via sa méthode local() permet d'insérer le résultat de la méthode LoadImage en toute quiétude vis à vis de la concurrence. Pour ce faire chaque tâche engendrée par la méthode parallel_for_each va tirer parti du thread natif qui l'héberge pour stocker localement les résultats engendrés à travers les structures de stockage relatives aux threads: Thread Local Storage. Naturellement après avoir terminé la méthode parallel_for_each nous devons fusionner les résultats intermédiaires de manière à obtenir une seule liste d'images.
voidImagesAgent::TwistImages(list<TypeBitmap>& images)
{
if (images.size())
{
parallel_for_each(images.begin(), images.end(), [&](TypeBitmap bmp)
{
send(m_outputBitmap, TwistImage(bmp));
});
images.clear();
}
}Dans la méthode TwistImages on retrouve l'usage de parallel_for_each. Le corps de lambda appelle directement la méthode send de la librairie Agent. Ici pas de problème de synchronisation les images sont totalement indépendantes. Nous sommes ici dans le traitement de la méthode la plus couteuse: TwistImage 660 ms. Le résultat de chaque appel à la méthode TwistImage sera repoussé immédiatement en asynchrone dans notre cas au bloc CreatePhoto.
voidImagesAgent::setup_network(unbounded_buffer<TypeBitmap>& outputTarget)
{
m_outputTarget = &outputTarget;
create_photo = new transform<TypeBitmap, TypeBitmap>(CreatePhoto);
m_outputBitmap.link_target(create_photo);
create_photo->link_target(m_outputTarget);
}Le bloc CreatePhoto à été instancié comme le montre le code ci-dessus. La librairie agent offre plusieurs blocs dont le bloc de transformation permettant de réaliser en asynchrone un appel à une méthode de transformation d'un type vers un autre type. Ici les deux types sont identiques car la méthode CreatePhoto réclame le même en entrée et en sortie. Une fois le transformateur instancié il faut le chaîner avec le reste du réseau de messages.
PhotoSticker* ImagesLoader::receive_imagesParallel(int& imagesLeft)
{
PhotoSticker* photoSticker = NULL;
if (m_nImages != 0)
{
TypeBitmap bmpTwisted;
if (try_receive(m_inputStickers, bmpTwisted))
{
m_nImages--;
photoSticker = CreateSticker(bmpTwisted);
}
}
imagesLeft = m_nImages;
return photoSticker;
}A la sortie du bloc CreatePhoto, les messages sont routés vers le buffer d'entré m_inputStickers. Comme la méthode receive_imagesParallel de la classe ImagesLoader est presque directement appelée par la méthode de rappel WndProc, nous utiliserons encore une fois l’appel try_receive permettant de tester si un message est présent ou non. Nous effectuons notre dernière étape, CreateSticker qui n'est pas couteuse et qui nous retourne une petite vignette prête à être affichée.
|
![]()
|
Après avoir recompilé l'ensemble, nous pouvons constater le résultat de nos efforts : 13 secondes en parallèle contre 35 en séquentiel. Nous avons donc obtenu sur cette petite application une amélioration significative.
A une époque où nous sommes de plus en plus sensibilisés à réduire notre consommation électrique, on peut trouver inopportun de chercher à paralléliser du code sur des machines pleines de cœurs. Je vous propose d'examiner la consommation en Watt de notre petite application avant et après parallélisation.
|
|
Pour mesurer l'ensemble je me suis procuré un petit Wattmètre que j'ai branché à la sortie de l'alimentation de mon portable Dell Precision M4400 incorporant un processeur Intel Core 2 Extreme Quad Core QX9300.
D'un côté nous avons 54 Watts pendant 35 secondes et 75 Watts pendant 13 secondes. Nous avons donc une consommation instantanée 1,5 fois plus élevée en mode parallèle pour un temps d'exécution 3 fois plus court.
Watts | Secondes | Watt / heure | |
Série | 54 | 35 | 0,52 |
Parallèle | 75 | 13 | 0,27 |
Donc nous consommons en parallèle environ 2 fois moins d'énergie, ce qui est loin d'être négligeable. Attention aux idées reçues car un bon usage des technologies permettant d’exploiter les cœurs des prochains matériels permettra sans doute de diminuer sensiblement la consommation électrique de nos ordinateurs. Je vous invite pour aller plus loin à lire le billet d'Eric Vernié.
La migration parallèle de codes existants séquentiels concernera sans doute de nombreux projets lorsque Visual Studio 2010 sortira en version finale. Cependant, la mise à l'échelle de codes existants reste une aventure semée d'embuches. L'utilisation d'une démarche étagée en plusieurs étapes vous permettra de mieux apprécier l'effort à réaliser. Les offres parallèles de Visual Studio 2010 vous aideront sensiblement pour réaliser avec élégance votre implémentation parallèle, mais les étapes d'exploration et d'analyse du code seront à votre charge. J'aimerai rappeler qu'une fois votre application adaptée au parallélisme, votre investissement sera largement bénéficiaire car votre application profitera automatiquement de l'évolution du nombre de cœurs. En d'autres mots vous aurez retrouvé les bénéfices de la loi de Moore.
Enfin, si votre entourage vous annonce que l'emprunte écologique de vos programmes une fois parallélisés sera catastrophique, je vous invite à citer les chiffres de notre petit exemple, Plus sérieusement, les constructeurs de microprocesseurs préparent leurs prochaines offres multi-cœurs dont les consommations seront hautement adaptatives, pouvant baisser considérablement le nombre de Watts consommés dans les périodes d’inactivité permettant même de remonter au système d'exploitation, des informations offrant l'opportunité à vos applications d'adopter une démarche plus smart en fonction des évènements courants.
· blog officiel en français dédié au développement parallèle
· Le livre de Joe Duffy sur la programmation concurrente sous Windows
· Le livre Patterns for Parallel Programming
· Sessions de la PDC08 consacrées au développement parallèle
![]() | Bruno Boucard (boucard.bruno@free.fr ) est spécialisé dans les technologies Microsoft. Il anime avec d'autres spécialistes le blog Développement parallèle de Microsoft France. |