Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Traitement des données : parallélisme et performances
Johnson M. Hart
Traiter des collectes de données constitue une tâche informatique fondamentale et certains problèmes pratiques présentent un parallélisme inhérent, ce qui permet potentiellement d'améliorer les performances et le débit sur des systèmes multicœurs. Je vais comparer plusieurs méthodes distinctes basées sur Windows visant à résoudre des problèmes présentant un haut degré de parallélisme des données.
Le test d'évaluation utilisé pour cette comparaison est un problème de recherche (« Geonames ») tiré du chapitre 9 du livre de Troy Magennis, « LINQ to Objects Using C# 4.0 » (LINQ pour objets à l'aide de C# 4.0) (Addison-Wesley, 2010). Solutions alternatives :
- Parallel Language Integrated Query (PLINQ) et C# 4.0, avec et sans améliorations du code d'origine.
- Code Windows natif à l'aide de C, des API Windows, des threads et des fichiers mappés en mémoire.
- Code multithread de Microsoft .NET Framework avec Windows C#.
Le code source de toutes les solutions est disponible sur mon site Web (jmhartsoftware.com). Je n'examinerai pas directement d'autres techniques de parallélisme, telles que Windows TPL (Task Parallel Library), même si PLINQ est disposé en couches sur TPL.
Comparaison et évaluation des solutions alternatives
Les critères d'évaluation des solutions sont, par ordre d'importance :
- Des performances totales en termes de temps écoulé pour réaliser la tâche.
- L'évolutivité du degré de parallélisme (nombre de tâches), nombre de cœurs et taille de la collecte de données.
- La simplicité du code, son élégance, sa facilité d'entretien et autres facteurs intangibles similaires.
Résumé des résultats
Cet article va démontrer que, pour un problème de recherche du test d'évaluation représentatif :
- Vous pouvez exploiter avec succès des systèmes multicœurs 64 bits pour améliorer les performances dans de nombreux problèmes de traitement des données et que PLINQ peut faire partie de la solution.
- Des performances concurrentielles et évolutives de PLINQ nécessitent des objets de collectes de données indexées. Prendre en charge l'interface IEnumerable ne suffit pas.
- Les solutions de code C#/.NET et natif sont les plus rapides.
- La solution PLINQ d'origine est plus lente d'un facteur de pratiquement 10 et il n'est pas évolutif au-delà de deux tâches, tandis que les autres solutions le sont jusqu'à six tâches avec six cœurs (maximum testé). Cependant, les améliorations du code améliorent aussi la solution d'origine de façon importante.
- Le code PLINQ est le plus simple et le plus élégant à tous les niveaux car LINQ fournit une possibilité de requête déclarative pour les données résidant sur la mémoire et les données externes. Le code natif n'apporte rien et le code C#/.NET est considérablement meilleur mais pas aussi simple que le code PLINQ.
- Toutes les méthodes évoluent bien avec une taille de fichier allant jusqu'aux limites de la mémoire physique du système de test.
Le problème du test d'évaluation : Geonames
L'idée de cet article est venue du chapitre 9 du livre LINQ de Magennis qui fait une démonstration de PLINQ en faisant des recherches dans une base de données géographiques contenant plus de 7,25 millions de noms de lieux dans un fichier de 825 Mo (plus d'un lieu pour 1 000 personnes). Chaque nom de lieu est représenté par un enregistrement de ligne de texte UTF-8 (longueur variable) (en.wikipedia.org/wiki/UTF-8) contenant plus de 15 colonnes de données séparées par des onglets. Remarque : Le codage UTF-8 assure qu'une valeur de tabulation (0x9) ou de renvoi à la ligne (0xA) ne peut se produire dans le cadre d'une séquence multi-octets. Ceci est essentiel pour plusieurs implémentations.
Le programme Geonames de Magennis implémente une requête codée en dur afin d'identifier tous les lieux ayant une altitude (colonne 15) supérieure à 8 000 mètres, en affichant le nom du lieu, le pays et l'altitude, le tout trié par altitude en ordre décroissant. Au cas où vous vous poseriez la question, ces lieux sont au nombre de 16 et l'Everest est le plus élevé, à 8 848 mètres.
Magennis fait état de temps écoulés de 22,3 secondes (un seul cœur) et de 14,1 secondes (deux cœurs). Une expérience précédente (par exemple, mon article « Windows Parallelism, Fast File Searching and Speculative Processing » (Parallélisme Windows, recherche rapide de fichiers et traitement spéculatif) sur informit.com/articles/article.aspx?p=1606242) démontre que les fichiers de cette taille peuvent être traités en quelques secondes et que les performance évoluent bien en fonction du nombre de cœurs. Par conséquent, j'ai décidé d'essayer de reproduire cette expérience et aussi d'améliorer le code PLINQ de Magennis pour obtenir de meilleures performances. Les améliorations apportées au PLINQ initial ont pratiquement doublé les performances mais sans améliorer l'évolutivité. D'autres apports, cependant, ont amené ces performances pratiquement au niveau du code natif et du code C# multithread.
Ce test d'évaluation est intéressant pour plusieurs raisons :
- Le sujet (lieux géographiques et attributs) est intéressant en lui-même et il est facile de généraliser la requête.
- Il existe un haut degré de parallélisme des données. En principe, chaque enregistrement devrait être traité en même temps.
- La taille de fichier est modeste par rapport à ce qui existe aujourd'hui, mais il est simple de tester des fichiers plus grands simplement en concaténant le fichier allCountries.txt de Geonames avec lui-même à plusieurs reprises.
- Le traitement n'est pas sans état. Il est nécessaire de déterminer les frontières de ligne et de champ afin de partitionner le fichier et les lignes doivent être traitées pour identifier chaque champ.
Hypothèse : Nous imaginons que le nombre d'enregistrements identifiés (dans ce cas, les lieux de plus de 8 000 mètres) est faible, afin que le temps de tri et d'affichage reste minimal comparé au temps de traitement général qui implique d'examiner chaque octet.
Autre hypothèse : Les résultats des performances représentent le temps requis pour traiter les collectes de données résidant dans la mémoire, telles que les données produites à une étape précédente du programme. Le programme de test d'évaluation lit bien un fichier, mais les programmes de test sont exécutés plusieurs fois afin de s'assurer que le fichier est bien résidant dans la mémoire. Je mentionnerai cependant le temps nécessaire au chargement initial du fichier qui est approximativement le même pour toutes les solutions.
Comparaison de performances
Le premier système de test est un système de bureau sous Windows 7 (AMD Phenom II, 2,80 GHz, 4 Go de RAM). Je présenterai plus tard les résultats obtenus avec trois autres systèmes en hyper-threading (ou HT) (en.wikipedia.org/wiki/Hyper-threading), ainsi qu'avec divers nombres de cœurs.
La figure 1 montre les résultats obtenus pour six solutions Geonames différentes, avec un temps écoulé (en secondes) présenté comme fonction du « Degré de parallélisation », ou DoP (nombre de tâches parallèles, configurable à un chiffre supérieur au nombre de processeurs). Le système de test a six cœurs mais les implémentations contrôlent le DoP. Le nombre de six tâches est optimal. Au-delà de six tâches, les performances sont dégradées. Tous les tests utilisent le fichier de données d'origine allCountries.txt de Geonames, de 825 Mo.
Figure 1 Performances de Geonames comme fonction du degré de parallélisme
Les implémentations sont (explications plus complètes à suivre) :
- Geonames Original. Il s'agit de la solution PLINQ d'origine de Magennis. Les performances ne sont pas concurrentielles et n'évoluent pas avec le nombre de processeurs.
- Geonames Helper. Version de Geonames Original aux performances améliorées.
- Geonames MMChar. Tentative infructueuse d'améliorer Geonames Helper à l'aide d'une classe de fichiers mappée en mémoire similaire à celle utilisée dans Geonames Threads. Remarque : Le mappage en mémoire autorise le référencement d'un fichier comme s'il était en mémoire sans opérations E/S explicites. Il peut aussi apporter des avantages de performances.
- Geonames MMByte. Cette solution modifie MMChar afin de traiter chaque octet du fichier d'entrée, tandis que les trois solutions précédentes convertissaient les caractères UTF-8 en Unicode (à 2 octets chacun). Les performances obtenues sont les meilleures de ces quatre premières solutions et sont plus que deux fois supérieures à celles de Geonames Original.
- Geonames Threads n'utilise pas PLINQ. Il s'agit de l'implémentation C#/.NET à l'aide de threads et d'un fichier mappé en mémoire. Les performances sont plus rapides qu'Index (l'élément suivant) et environ équivalentes à Native. Cette solution et Geonames Native offrent la meilleure évolutivité de parallélisme.
- Geonames Index. La solution PLINQ prétraite le fichier de données (ce qui prend environ neuf secondes) pour créer un objet de liste<byte[]> résidant sur la mémoire pour le traitement PLINQ suivant. Le coût de prétraitement peut être amorti sur plusieurs requêtes, ce qui offre des performances seulement légèrement inférieures à celles de Geonames Native et Geonames Threads.
- Geonames Native (n'apparaît pas sur la figure 1) n'utilise pas PLINQ. Il s'agit de l'implémentation API de Windows en langage C à l'aide de threads et d'un fichier mappé en mémoire comme dans le chapitre 10 de mon livre, « Windows System Programming » (Programmation d'un système Windows) (Addison-Wesley, 2010). Une optimisation complète du compilateur est essentielle pour ces résultats. L'optimisation par défaut n'offre qu'environ la moitié des performances.
Toutes les implémentations sont des versions 64 bits. Les versions 32 bits fonctionnent dans la plupart des cas, mais elles ne fonctionnent pas pour les fichiers plus grands (voir la figure 2). La figure 2 montre les performances obtenues à l'aide de DoP 4 et de fichiers plus grands.
Figure 2 Performances de Geonames comme fonction de la taille de fichier
Le système de test dans ce cas est équipé de quatre cœurs (AMD Phenom Quad-Core, 2,40 GHz, 8 Go de RAM). Les fichiers les plus grands ont été créés en concaténant plusieurs copies du fichier d'origine. La figure 2 montre juste les trois solutions les plus rapides, notamment Geonames Index, la solution PLINQ la plus rapide (sans compter le prétraitement des fichiers), et les évolutions de performances avec la taille de fichier aux limites de la mémoire physique.
Je vais à présent décrire les implémentations deux à sept et aborder les techniques PLINQ plus en détails. Ensuite, j'aborderai les résultats obtenus sur d'autres systèmes de test et résumerai ce qui en ressort.
Les solutions PLINQ améliorées : Geonames Helper
La figure 3 montre Geonames Helper et les modifications que j'ai apportées (en gras) au code de Geonames Original.
Figure 3 Geonames Helper avec modifications en surbrillance apportées au code PLINQ d'origine
class Program
{
static void Main(string[] args)
{
const int nameColumn = 1;
const int countryColumn = 8;
const int elevationColumn = 15;
String inFile = "Data/AllCountries.txt";
if (args.Length >= 1) inFile = args[0];
int degreeOfParallelism = 1;
if (args.Length >= 2) degreeOfParallelism = int.Parse(args[1]);
Console.WriteLine("Geographical data file: {0}.
Degree of Parallelism: {1}.", inFile, degreeOfParallelism);
var lines = File.ReadLines(Path.Combine(
Environment.CurrentDirectory, inFile));
var q = from line in
lines.AsParallel().WithDegreeOfParallelism(degreeOfParallelism)
let elevation =
Helper.ExtractIntegerField(line, elevationColumn)
where elevation > 8000 // elevation in meters
orderby elevation descending
select new
{
elevation = elevation,
thisLine = line
};
foreach (var x in q)
{
if (x != null)
{
String[] fields = x.thisLine.Split(new char[] { '\t' });
Console.WriteLine("{0} ({1}m) - located in {2}",
fields[nameColumn], fields[elevationColumn],
fields[countryColumn]);
}
}
}
}
Puisque de nombreux lecteurs ne connaissent peut-être pas bien PLINQ et C# 4.0, j'ajouterai quelques commentaires sur la figure 3, notamment les descriptions des améliorations :
- Les lignes 9 à 14 permettent à l'utilisateur de spécifier le nom du fichier d'entrée et le degré de parallélisme (le nombre maximum de tâches simultanées) sur la ligne de commande. Ces valeurs sont codées en dur dans l'original.
- Les lignes 16 et 17 commencent à lire les lignes du fichier de manière asynchrone et tapent implicitement les lignes sous la forme d'un tableau de chaînes C#. Les valeurs des lignes ne sont pas utilisées avant les lignes 19 à 27. D'autres solutions, telles que Geonames MMByte, utilisent une autre classe avec leur propre méthode ReadLines et ces lignes de code sont les seules à devoir être modifiées.
- Les lignes 19 à 27 sont du code LINQ auquel on a ajouté l'extension AsParallel PLINQ. Ce code ressemble à du SQL, et la variable « q » est tapée implicitement sous la forme d'un tableau d'objets composé d'une élévation d'entiers et d'une chaîne. Notez que PLINQ effectue tout le travail de gestion des threads, la méthode AsParallel suffit pour convertir le code LINQ de série en code PLINQ.
- Ligne 20. La figure 4 montre la méthode Helper.ExtractIntegerField. Le programme original utilise la méthode String.Split d'une manière équivalente à celle utilisée pour afficher les résultats de la ligne 33 (Figure 3). C'est là qu'est la clé des performances améliorées de Geonames Helper comparé à Geonames Original, puisqu'il n'est plus nécessaire d'allouer les objets String de chaque champ de chaque ligne.
Figure 4 Classe de Geonames Helper et méthode ExtractIntegerField
class Helper
{
public static int ExtractIntegerField(String line, int fieldNumber)
{
int value = 0, iField = 0;
byte digit;
// Skip to the specified field number and extract the decimal value.
foreach (char ch in line)
{
if (ch == '\t') { iField++; if (iField > fieldNumber) break; }
else
{
if (iField == fieldNumber)
{
digit = (byte)(ch - 0x30); // 0x30 is the character '0'
if (digit >= 0 && digit <= 9)
{ value = 10 * value + digit; }
else // Character not in [0-9]. Reset the value and quit.
{ value = 0; break; }
}
}
}
return value;
}
}
Notez que la méthode AsParallel utilisée à la ligne 19 peut être utilisée avec un objet IEnumerable. Comme mentionné plus haut, la figure 4 montre la méthode ExtractIntegerField de la classe Helper. Elle extrait et évalue simplement le champ spécifié (l'altitude dans ce cas), ce qui évite d'utiliser des méthodes de bibliothèques pour de meilleures performances. La figure 1 montre que cette amélioration multiplie par deux les performances obtenues avec DoP 1.
Geonames MMChar et Geonames MMByte
Geonames MMChar représente une tentative infructueuse d'améliorer les performances par le mappage en mémoire du fichier d'entrée à l'aide d'une classe personnalisée, FileMmChar. Geonames MMByte, cependant, produit des avantages importants, puisque les octets du fichier d'entrée ne sont pas étendus en Unicode.
MMChar requiert une nouvelle classe, FileMmChar, qui prend en charge l'interface IEnumerable<String>. La classe FileMmByte est similaire et traite des objets byte[] plutôt que des objets String. La seule modification de code importante Figure 3, a été apportée aux lignes 16 et 17, qui sont à présent :
var lines = FileMmByte.ReadLines(Path.Combine(
Environment.CurrentDirectory, inFile));
Le code de
public static IEnumerable<byte[]> ReadLines(String path)
qui prend en charge l'interface IEnumerable<byte[]> dans FileMmByte construit un objet FileMmByte et un objet IEnumerator<byte[]> qui analyse le fichier mappé à la recherche de lignes individuelles.
Notez que les classes FileMmChar et FileMmByte ne sont pas « sûres » car elles créent et utilisent des pointeurs pour accéder aux fichiers et elles utilisent l'interopérabilité du code C#/natif. Toute l'utilisation du pointeur, cependant, est isolée dans un ensemble distinct et le code utilise des tableaux plutôt qu'un déréférencement du pointeur. La classe MemoryMappedFile de .NET Framework 4 n'est d'aucune aide car il faut utiliser les fonctions d'accès pour déplacer les données de la mémoire mappée.
Geonames Native
Geonames Native exploite les API Windows, les threads et le mappage en mémoire des fichiers. Les modèles de code de base sont décrits dans le chapitre 10 de « Windows System Programming » (Programmation d'un système Windows). Le programme doit gérer directement les threads et doit également mapper avec soin le fichier en mémoire. Les performances sont bien meilleures que toutes les implémentations PLINQ, à l'exception de Geonames Index.
Il existe, cependant, une distinction importante à faire entre le problème Geonames et une recherche de fichier simple et sans état ou une transformation. Le défi consiste à déterminer la bonne méthode pour partitionner les données d'entrée de manière à attribuer des partitions différentes à des tâches différentes. Il n'existe aucun moyen évident de déterminer les frontières des lignes sans analyser tout le fichier, attribuer une partition à taille fixe à chaque tâche n'est donc pas réalisable. Cependant, la solution est simple, lorsqu'elle est illustrée avec DoP 4.
- Divisez le fichier d'entrée en quatre partitions égales, en communiquant le lieu de départ de la partition à chaque thread dans le cadre de l'argument de fonction du thread.
- Faites traiter toutes les lignes (enregistrements) qui débutent dans la partition à chaque thread. Ceci signifie qu'un thread va probablement analyser la partition suivante afin de terminer le traitement de la dernière ligne qui commence dans la partition.
Geonames Threads
Geonames Threads utilise la même logique que Geonames Native. En fait, une partie du code est identique ou quasi identique. Cependant, des expressions lambda, des méthodes d'extension, des conteneurs et d'autres fonctionnalités C#/.NET simplifient allègrement le codage.
Comme MMByte et MMChar, le mappage en mémoire du fichier nécessite des classes « non sûres » et l'interopérabilité du code C#/natif afin d'utiliser des pointeurs sur la mémoire mappée. L'effort sera cependant récompensé car les performances de Geonames Threads sont les mêmes que celles de Geonames Native avec un code bien plus simple.
Geonames Index
Les résultats PLINQ (Original, Helper, MMChar et MMByte) sont décevants en comparaison des résultats de Native et de .NET Threads. Existe-t-il une manière d'exploiter la simplicité et l'élégance de PLINQ sans sacrifier les performances ?
Bien qu'il soit impossible de déterminer exactement la manière de traiter la requête de PLINQ (lignes 16 à 27 de la figure 3), il est probable que PLINQ ne présente aucune manière acceptable de partitionner les lignes d'entrée pour faire effectuer un traitement parallèle par des tâches distinctes. Nous allons prendre comme hypothèse de travail que le partitionnement est la cause probable des problèmes de performances de PLINQ.
Dans le livre de Magennis (pp. 276-279), le tableau de chaînes de lignes prend en charge l'interface IEnumerable<String> (voir également le livre de John Sharp, « Microsoft Visual C# 2010 Step by Step » (Microsoft Visual C# 2010 étape par étape) [Microsoft Press, 2010], chapitre 19). Cependant, les lignes ne sont pas indexées, PLINQ utilise alors probablement le « partitionnement par segments ». De plus, les méthodes IEnumerator.MoveNext pour les classes FileMmChar et FileMmByte sont lentes en raison de leur besoin d'analyser chaque caractère jusqu'à la localisation de la ligne suivante.
Que se passerait-il si le tableau de chaînes de lignes était indexé ? Pourrions-nous améliorer les performances de PLINQ, en particulier lorsqu'il est complété par la mémoire qui mappe le fichier d'entrée ? Geonames Index indique que cette technique améliore réellement les performances, offrant des résultats comparables au code natif. En général, cependant, soit il existe un coût initial nécessaire visant à déplacer les lignes dans une liste ou un tableau interne à la mémoire, indexé (le coût peut ensuite être amorti sur plusieurs requêtes), soit le fichier ou autre source de données est déjà indexé, peut-être au cours de la génération lors d'une étape précédente du programme, éliminant de fait le coût de prétraitement.
L'opération d'indexation initiale est simple. Il suffit d'accéder à chaque ligne l'une après l'autre, puis d'ajouter la ligne à une liste. Utilisez l'objet list des lignes 16 et 17, comme illustré à la figure 3, ainsi que dans cet extrait de code, qui démontre le prétraitement :
// Preprocess the file to create a list of byte[] lines
List<byte[]> lineListByte = new List<byte[]>();
var lines =
FileMmByte.ReadLines(Path.Combine(Environment.CurrentDirectory, inFile));
// ... Multiple queries can use lineListByte
// ....
foreach (byte[] line in lines) { lineListByte.Add(line); }
// ....
var q = from line in lineListByte.AsParallel().
WithDegreeOfParallelism(degreeOfParallelism)
Notez que traiter encore les données en convertissant la liste en tableau est légèrement plus efficace, bien que cette méthode augmente le temps de prétraitement.
Dernière amélioration des performances
Les performances de Geonames Index peuvent encore être améliorées en indexant les champs de chaque ligne afin que la méthode ExtractIntegerField n'ait pas besoin d'analyser tous les caractères d'une ligne vers le champ spécifié.
L'implémentation, Geonames IndexFields, modifie la méthode ReadLines afin qu'une ligne renvoyée soit un objet contenant à la fois un tableau byte[] et un tableau uint[] qui renferment les lieux de chaque champ. Le résultat en est une amélioration des performances de 33 % par rapport à Geonames Index, ce qui rapproche beaucoup ces performances de celles des solutions native et C#/.NET. (Geonames IndexFields est incluse avec le téléchargement de code). De plus, il est à présent bien plus facile de construire plus de requêtes générales puisque les champs individuels sont disponibles de suite.
Restrictions
Les solutions efficaces nécessitent toutes des données résidant dans la mémoire et les avantages de performances ne s'étendent pas aux collectes de données de très grande taille. « Très grande taille », dans ce cas, désigne des tailles de données qui approchent la taille de la mémoire physique du système. Dans l'exemple Geonames, le fichier de 3 302 Mo (quatre copies du fichier d'origine) pourrait être traité sur le système de test de 8 Go. Un test effectué sur huit copies concaténées du fichier, cependant, s'est révélé très lent, quelle que soit la solution.
Comme mentionné plus tôt, les performances sont également les meilleures lorsque les fichiers de données sont « présents », dans le sens où on y a accédé récemment et sont probablement encore dans la mémoire. La pagination du fichier de données au cours de l'exécution initiale peut prendre 10 secondes ou plus et est comparable à l'opération d'indexation dans l'extrait de code mentionné plus tôt.
En résumé, les résultats de cet article s'appliquent aux structures de données résidant dans la mémoire. Les tailles et prix de mémoire d'aujourd'hui permettent d'avoir des objets de données importants, comme par exemple un fichier de 7,25 millions de noms de lieux, qui résident dans la mémoire.
Résultats de systèmes de test supplémentaires
La figure 5 montre les résultats de test effectué sur un système supplémentaire (Intel i7 860, 2,80 GHz, quatre cœurs, huit threads, Windows 7, 4 Go de RAM). Le processeur prend en charge l'hyper-threading, ainsi les valeurs DoP testées sont 1, 2, ..., 8. La figure 1 se base sur un système de test disposant de six cœurs AMD qui ne prend pas en charge l'hyper-threading.
Figure 5 Intel i7 860, 2,80 GHz, quatre cœurs, huit threads, Windows 7, 4 Go de RAM
Deux configurations de test supplémentaires ont produit des résultats similaires (et toutes les données sont disponibles sur mon site Web) :
- Intel i7 720, 1,60 GHz, quatre cœurs, huit threads, Windows 7, 8 Go de RAM
- Intel i3 530, 2,93 GHz, deux cœurs, quatre threads, Windows XP64, 4 Go de RAM
Les caractéristiques de performances intéressantes comprennent :
- Geonames Threads fournit régulièrement les meilleures performances, comme Geonames Native.
- Geonames Index est la solution PLINQ la plus rapide, approchant les performances de Geonames Threads. Remarque : GeonamesIndexFields est légèrement plus rapide mais n'est pas présente dans la figure 5*.*
- Outre Geonames Index, toutes les solutions PLINQ évoluent négativement avec le DoP pour DoP supérieur à deux. C'est-à-dire que les performances baissent au fur et à mesure que le nombre de tâches parallèles augmente. Dans cet exemple, PLINQ produit de bonnes performances uniquement lorsqu'on l'utilise avec des objets indexés.
- La contribution aux performances de l'hyper-threading est marginale. Par conséquent, les performances de Geonames Threads et de Geonames Index n'augmentent pas de manière importante pour un DoP supérieur à quatre. Cette faible évolutivité de HT est peut-être une conséquence de la planification de deux threads sur des processeurs logiques du même cœur, plutôt que de s'être assuré qu'ils s'exécutent sur des cœurs distincts lorsque c'était possible. Cependant, cette explication n'apparaît pas plausible puisque Mark E. Russinovich, David A. Solomon et Alex Ionescu disent que les processeurs physiques sont planifiés avant les processeurs logiques à la page 40 de leur livre, « Windows Internals, Fifth Edition » (Éléments internes de Windows, cinquième édition) (Microsoft Press, 2009). Les systèmes AMD sans HT (Figure 1) ont fourni des performances trois à quatre fois supérieures avec un DoP supérieur à quatre, comparés aux résultats du DoP séquentiel de un de Threads, Native et Index. La figure 1 montre que les meilleures performances se produisent lorsque le DoP est identique au nombre de cœurs, là où les performances multithread sont de 4,2 fois celles du DoP de un.
Résumé des résultats
PLINQ offre un modèle excellent pour traiter les structures de données internes à la mémoire et il est possible d'améliorer les performances du code existant grâce à des modifications simples (par exemple, Helper) ou à l'aide de techniques plus avancées, comme démontré avec MMByte. Cependant, aucune des améliorations simples n'offre des performances approchant celles du code natif ou C#/.NET multithread. De plus, l'amélioration n'évolue pas avec le nombre de cœurs et le DoP.
PLINQ peut s'approcher des performances du code natif et C#/.NET, mais il nécessite d'utiliser des objets de données indexés.
Utilisation du code et des données
Tout le code est disponible sur mon site Web (jmhartsoftware.com/SequentialFileProcessingSupport.html) en suivant ces instructions :
- Rendez-vous à la page de téléchargement pour obtenir un fichier ZIP contenant le code PLINQ et le code de Geonames Threads. Toutes les variations de PLINQ se trouvent dans le projet GeonamesPLINQ (Visual Studio 2010, Visual Studio 2010 Express suffit). Geonames Threads se trouve dans le projet GeonamesThreads Visual Studio 2010. Ces projets sont tous deux configurés pour des versions 64 bits. Le fichier ZIP contient également un tableur renfermant les performances brutes utilisées dans les figures 1, 2 et 5. Un simple commentaire « Usage » en haut du fichier explique l'option de ligne de commande permettant de sélectionner le fichier d'entrée, le DoP et l'implémentation.
- Rendez-vous sur la page de support Windows System Programming (jmhartsoftware.com/comments_updates.html) afin de télécharger le code de Geonames Native et les projets (un fichier ZIP), où vous trouverez le projet Geonames. La structure est expliquée par un fichier ReadMe.txt.
- Téléchargez la base de données GeoNames sur download.geonames.org/export/dump/allCountries.zip.
Questions non traitées
Cet article a comparé les performances de plusieurs techniques alternatives visant à résoudre un même problème. L'approche choisie a consisté à utiliser les interfaces standard au cours de leur description et à imaginer un modèle simple de mémoire partagée pour les processeurs et les threads. Je n'ai cependant pas fait d'effort important pour étudier en profondeur les implémentations sous-jacentes ou les fonctionnalités spécifiques des machines de test, et nombreuses sont les questions qui pourraient être examinées à l'avenir. Voici quelques exemples :
- Quel est l'effet des échecs de cache et existe-t-il une méthode pour en réduire l'impact ?
- Quel serait l'impact des disques d'état solide ?
- Existe-t-il une manière de réduire l'écart de performances entre la solution PLINQ Index et les solutions Threads et Native ? Les expériences de réduction de la quantité de copies des données dans les méthodes FileMmByte IEnumerator.MoveNext et Current n'ont pas fait la preuve d'avantages conséquents.
- Les performances sont-elles proches du maximum théorique tel que déterminé par la bande passante de mémoire, la vitesse du processeur et d'autres fonctionnalités architecturales ?
- Existe-t-il une manière d'obtenir des performances évolutives sur les systèmes HT (voir la figure 5) qui soient comparables à celles des systèmes sans l'HT (figure 1) ?
- Est-il possible d'utiliser le profilage et les outils de Visual Studio 2010 pour identifier et supprimer les goulots d'étranglement de performances ?
J'espère que vous pourrez approfondir les recherches.
Johnson (John) M. Hart est consultant spécialisé dans l'architecture et le développement d'applications Microsoft Windows et Microsoft .NET Framework, la formation et la rédaction technique. Il dispose de nombreuses années d'expérience d'ingénieur système, de responsable ingénieur et d'architecte chez Cilk Arts Inc. (depuis rachetée par Intel Corp.), Sierra Atlantic Inc., Hewlett-Packard Co. et Apollo Computer. Il est professeur d'informatique depuis de nombreuses années et est l'auteur de quatre éditions de « Windows System Programming » (Programmation d'un système Windows) (Addison-Wesley, 2010).
Merci aux experts techniques suivants d’avoir relu cet article : Michael Bruestle,Andrew Greenwald, Troy Magennis et CK Park