Stratégies de programmation pour le traitement multicœur: pipelining

Aperçu

Dans un monde rempli de processeurs multicoeurs et des applications multithread, les programmeurs doivent constamment penser à la meilleure façon d’utiliser la puissance disponible des UC de pointe lors du développement de leurs applications. Bien que la structuration du code parallèle par des langages textuels traditionnels puisse être difficile à programmer et à visualiser, les environnements de développement graphique tels que LabVIEW de National Instruments permettent aux ingénieurs et scientifiques de diviser leurs temps de développement et d’implémenter rapidement leurs idées. Parce que LabVIEW est parallèle par nature (basé sur les flux de données), la programmation d’applications multithread est typiquement une tâche très simple. Des tâches indépendantes sur le diagramme s’exécutent automatiquement en parallèle, sans nécessiter de travail supplémentaire pour le programmeur. Mais qu’en est-il des parties de code qui ne sont pas indépendantes ? Lors de l’implémentation d’applications série par nature, que peut-il être fait pour exploiter la puissance des UC multicœurs ?

Contenu

Introduction au pipelining

Le pipelining est une technique largement adoptée pour l’amélioration des performances des tâches logicielles série. Simplement dit, le pipelining est le processus de division d’une tâche série en étapes concrètes, qui peuvent être exécutées à la manière d’une chaîne de montage.

Considérez l’exemple suivant : supposez que vous fabriquiez des voitures sur une chaîne de montage automatisée. Votre tâche finale est de construire une voiture complète, mais il est possible de séparer cette tâche en trois étapes pratiques : construire le châssis, ajouter les éléments à l’intérieur (comme le moteur) et peindre la voiture lorsqu’elle est terminée.

Supposez que la construction du châssis, l’ajout des éléments et la peinture prennent une heure chacun. Ainsi, si vous ne construisez qu’une seule voiture à la fois, il faudra trois heures pour la terminer (voir la Figure n°1 ci-dessous).

Figure n°1. Dans cet exemple (sans pipelining), la construction d’une voiture complète prend 3 heures.

Comment améliorer ce procédé ? Que se passe-t-il si l’on construit une station pour la construction du châssis, une autre pour l'installation des éléments et une troisième pour la peinture ? Désormais, pendant qu’une voiture est peinte, on peut installer les éléments d’une deuxième et une troisième peut être en cours de construction du châssis.

Pipelining de base en LabVIEW

Le même concept de pipelining que celui illustré par l’exemple de la voiture peut être appliqué à n’importe quelle application LabVIEW dans laquelle une tâche série est exécutée. Essentiellement, il est possible d’utiliser les registres à décalage et les nœuds de rétro-action LabVIEW pour réaliser une "ligne d’assemblage", à partir de n’importe quel programme donné. L’illustration suivante montre comment une application d’acquisition de données peut être sérialisée afin d’utiliser le pipelining et s’exécuter sur les différents cœurs de l'UC :

 Figure n°3. Diagramme d’exécution en fonction du temps d’une application sérialisée sur plusieurs cœurs.

Préoccupations importantes

Lors de la création d’applications multicœurs qui mettent en œuvre le pipelining, le développeur  doit prendre en compte plusieurs points importants. En particulier, l'équilibre des étapes du pipeline ainsi que la minimisation des transferts mémoire entre les cœurs sont critiques pour l’obtention de gains de performances avec le pipelining.

Équilibre des étapes

Dans les exemples de fabrication de voiture et les exemples LabVIEW précédents, on a supposé que l’exécution de chaque étape du pipeline prenait une quantité égale de temps ; ces étapes de pipeline étaient équilibrées. Cependant, dans les applications réelles, cette situation se produit rarement. Considérez le diagramme ci-dessous ; si l’Étape 1 prend trois fois plus de temps pour s’exécuter que l'Étape 2, le pipelining des deux étapes ne produit qu’une augmentation minimum des performances.

Sans pipeline (temps total = 4 s) :

Avec pipeline (temps total = 3 s) :

Remarque : amélioration des performances = 1,33 X (ce n’est pas un cas idéal pour le pipelining)

Pour remédier à cette situation, le programmeur doit transférer des tâches de l’Étape 1 à l’Étape 2, jusqu’à ce que les tâches mettent environ le même temps pour s’exécuter. Avec un grand nombre d’étapes de pipeline, cela peut s’avérer difficile.

En LabVIEW, il est utile d’évaluer les performances de chacune des étapes du pipeline, pour en assurer l’équilibre. Ceci peut être le plus facilement réalisé grâce à une structure séquence déroulée, en conjonction avec la fonction Tick Count (ms), comme indiqué à la Figure n°4.

 Figure n°4. Mesurez les temps d’exécution des étapes du pipeline pour en assurer l’équilibre.

Transfert de données entre les cœurs

Lorsque cela est possible, il convient d’éviter le transfert d’importantes quantités de données entre les étapes du pipeline. Puisque les étapes d’un pipeline donné pourraient être exécutées sur des cœurs séparés, tout transfert de données entre les étapes individuelles pourrait réellement provoquer un transfert de mémoire entre les cœurs physiques du processeur. Dans le cas où deux cœurs ne partagent pas de cache (ou que la taille du transfert dépasse la taille du cache), l’utilisateur final de l’application peut observer une diminution de l’efficacité du pipelining.

Conclusion

Pour résumer, le pipelining est une technique que les développeurs peuvent utiliser pour obtenir une augmentation des performances des applications qui sont séquentielles par nature (sur des machines multicœurs ). La tendance de l’industrie des microprocesseurs qui est à l’augmentation du nombre de cœurs par processeur signifie que les stratégies telles que le pipelining vont devenir essentielles pour le développement d’applications dans un futur proche.

De manière à gagner le plus de performances par le pipelining, les étapes individuelles doivent être équilibrées avec précaution. Aucune étape individuelle ne doit mettre plus de temps que les autres. De plus, tout transfert de données entre les étapes doit être minimisé car alors une zone mémoire unique doit être accédée par plusieurs cœurs ce qui se traduit par une diminution des performances.