Optimisation de code et parallélisme
Georges-André Silber, École des mines de Paris
Cours
Travaux pratiques: "Catch the Speedup"
L'objectif ce ces travaux pratiques est d'améliorer les perfomances d'un code C qui transforme des images, à la fois le coeur de la transformation (ce qui dans le code C que vous verrez ci-dessous est mesuré entre start_counter()
et get_counter()
) et le traitement global de toutes les images (mesuré simplement avec time
).
A priori, on ne connait pas la manière la plus rapide de faire tourner ce code: le gagnant de ce concours "Catch the Speedup" sera celle ou celui qui a obtenu la meilleure accélération du code.
Préliminaires
- Récupérer le code C et le compiler sur votre machine avec GCC, LLVM ou un autre compilateur C de votre choix.
- La compilation du code se fait avec la commande
make
si vous êtes sous Linux (ou WSL2) ou macOS avec processeur Intel, ou avecmake -f Makefile.clock
sir vous être sous macOS avec processeur Arm. - Récupérer les données image.
-
Exécuter le code compilé précédemment de la façon suivante:
time ./transform_image $IMAGES/transfo.txt
oùIMAGES
est une variable d'environnement qui pointe vers le répertoire où se trouvent les images récupérées précédemment:export IMAGES=chemin-vers-les-images
L'exécution devrait afficher quelque chose comme:image1.pgm courbe1.amp 5 image1_t.pgm image1.pgm: 5617 x 3684 = 20693028 pixels 1909743856.000000 clock cycles. image2.pgm courbe2.amp 5 image2_t.pgm image2.pgm: 5227 x 3515 = 18372905 pixels 1651530808.000000 clock cycles. image3.pgm courbe3.amp 5 image3_t.pgm image3.pgm: 6660 x 9185 = 61172100 pixels 6592744002.000000 clock cycles. image4.pgm courbe4.amp 4 image4_t.pgm image4.pgm: 3381 x 4914 = 16614234 pixels 1252728815.000000 clock cycles. image5.pgm courbe5.amp 7 image5_t.pgm image5.pgm: 3226 x 3255 = 10500630 pixels 1119565617.000000 clock cycles. image6.pgm courbe6.amp 6 image6_t.pgm image6.pgm: 3677 x 3677 = 13520329 pixels 1163077323.000000 clock cycles. image7.pgm courbe7.amp 9 image7_t.pgm image7.pgm: 3264 x 4896 = 15980544 pixels 1183738944.000000 clock cycles. image8.pgm courbe8.amp 5 image8_t.pgm image8.pgm: 1757 x 2636 = 4631452 pixels 443741534.000000 clock cycles. image9.pgm courbe9.amp 7 image9_t.pgm image9.pgm: 2498 x 3330 = 8318340 pixels 639985780.000000 clock cycles. image10.pgm courbe10.amp 9 image10_t.pgm image10.pgm: 3024 x 3024 = 9144576 pixels 572600880.000000 clock cycles. TOTAL: 16529457559.000000 clock cycles. real 0m57.843s user 0m50.297s sys 0m1.763s
- Lire le code et comprendre ce qu'il fait.
Optimisations
À chaque modification, relancer le code et mesurer le gain obtenu. Garder trace de chaque version de votre code avec le gain obtenu. Recommendations:
- Utiliser moins de fonctions
- Mieux utiliser la mémoire (localité)
- Utiliser moins de boucles
- Vous pouvez vous aider de perf (si vous êtes sous Linux) pour analyser plus finement le comportement de votre code. Sous WSL2, vous pouvez suivre ces instructions pour installer perf.
Parallélisation(s)
Voici des pistes de parallélisation, qui peuvent être complémentaires:
- Vectorisation: utiliser les instructions vectorielles, soit en optimisant l'assembleur "à la main", soit en utilisant les intrinsics x86 présents dans GCC, soit en utilisant les bons paramètres du compilateur. Cette étape nécessitera certainement d'aider le compilateur en lui présentant des boucles "facilement" vectorisables. Vérifiez également que GCC génère du code correspondant à votre machine. La documentation Intel sur l'optimisation de code pour architecture x86-64 donne de nombreuses pistes d'optimisation.
- Multithreads: utiliser OpenMP pour obtenir une version du code utilisant plusieurs threads. Mesurer le gain obtenu en fonction du nombre de threads utilisés.
- Multiprocessus: modifier le code en utilisant plusieurs processus. On peut imaginer ici que l'on utilise des processus en parallèle où chaque processus s'occuperait d'une image. Ainsi, on pourra cherche ici à améliorer le temps pris par le traitement des dix images à la suite. Se rappeler de fork().
- GPU: utiliser CUDA afin d'obtenir une version du code fonctionnant sur un processeur graphique.