Utilisation des extensions SIMD du pentium pour le calcul
Transcription
Utilisation des extensions SIMD du pentium pour le calcul
5MMCEAMC/SLE-3A Travaux Pratiques Utilisation des extensions SIMD du pentium pour le calcul de la conversion YCbCr vers RGB Frédéric Pétrot Durée : 3 heures 1 Organisation Le travail se fait en binôme cette fois-ci (l’expérience forme la jeunesse). Le sujet est découpé en parties qui peuvent au départ être faites plus ou moins indépendamment, aussi il est raisonnable que les 2 membres du binôme travaillent en parallèle (de degré 2 ici). Merci de m’envoyer votre fichier conv.c au plus tard lundi soir minuit, c’est là dessus que vous serez notés (ou pas, ...). 2 Introduction Ce TP a pour objectif de mettre en pratique l’utilisation des opérations SIMD de base (dites MMX) du processeur x86. SIMD signifie Single Instruction, Multiple Data, ce qui veut dire que la même opération va être exécutée sur n données différentes (et indépendantes) simultanément. Un exemple simple est l’exécution d’une addition sur les 4 octets d’un mots de 32 bits considérés indépendamment. Les instructions MMX d’Intel travaillent sur des registres de 64 bits dont le contenu peut être considéré comme soit 8 octets (de 8 bits, dits bytes), 4 entiers courts de 16 bits (appelés words par Intel), 2 entiers de 32 bits (appelés doubles), ou 1 entier long de 64 bits (encore dit quad). Il y a 57 instructions MMX, mais le travail demandé ne requière l’utilisation que d’un petit sous-ensemble. Pour information, je n’ai utilisé que les 15 suivantes dans ma correction : emms movd movl movq packuswb paddw pmulhw pmullw psrlq psrlw psubw punpckhwd punpcklbw punpcklwd pxor Une rapide explication des instructions MMX se trouve sur le site http://tommesani.com/index. php/component/content/article/2-simd/34-mmx-primer.html. Attention, sur ce cite l’ordre des opérandes dans les instructions est celui de l’assembleur Intel, alors que par défaut GAS utilise l’ordre opposé : l’opérande 1 et le résultat sont à droite. Le TP utilise un décodeur vidéo Motion-JPEG (celui du projet C de 1ère année pour être précis), dans lequel nous allons tenter d’optimiser une fonction particulière, celle qui assure la conversion des composantes de luminance et chrominance en rouge, vert, bleue pour l’affichage dans un frame buffer. 3 3.1 Travail demandé Préliminaires Récupérez sur le site du cours (https://ensiwiki.ensimag.fr/images/d/d1/Tp2_src.tgz) l’archive contenant les sources nécessaires au TP. Cette archive s’expand dans le répertoire tp2_src/, et contient une vidéo ice_age_256x144_444.mjpeg qui sera notre benchmark, un Makefile, des fichiers objets contenant les différentes phases du décodage hormis la conversion YUV vers RGB, et trois fichiers sources contenant diverses implantations (dont certaines partielles) de la conversion. 1 conv-float.c la version initiale en virgule flottante ; conv-int.c la version en entiers ; conv-v4si.c la version avec le support vectoriel du compilateur gcc ; conv-loop4.c la version qui est le sujet de la première partie de la question 1 ; conv-unrolled4.c la version qui est le sujet de la deuxième partie de la question 1 ; conv-mmx.c la version qui est le sujet des questions 2 et suivantes. L’exécutable crée prend 1, 2 ou 3 arguments. Le premier est le nom de la vidéo, le second le nombre de frame à décoder, et le troisième n’importe quoi. En présence de ce troisième argument, le résultat du décodage n’est pas affiché (à 25 images par secondes), ainsi c’est la vitesse brute qui peut être mesurée. Afin de voir combien de temps prend le décodage (sur le film complet), on peut lancer : petrot@tilleul% time ./mjpeg-float ice_age_256x144_444.mjpeg -1 no ./mjpeg ../src/ice_age_256x144_444.mjpeg -1 no 3,88s user 0,02s system 99% cpu 3,905 total Question 1 Le MMX permettant d’exploiter le parallélisme des données, proposez une nouvelle version de cette fonction, toujours écrite en C, qui mette en évidence un parallélisme de degré 4 sur la boucle interne. Il n’est pas possible d’exprimer le parallélisme dans le C standard, aussi on utilisera des tableaux de taille 4. Dans un premier temps, on utilisera des boucles qui vont itérer de 0 à 3 sur ces tableaux. Relancez l’exécution pour vérifier que vos modifications n’ont pas d’impact sur le fonctionnement. Dans un second temps, on déroulera explicitement les boucles de 0 à 3, ce qui nécessite de la recopie de code. Il est clair que ce n’est pas une pratique de programmation recommandée, mais on fera une exception cette fois, c’est pour la bonne cause. Relancez l’exécution pour vérifier que le résultat est toujours Quelques informations supplémentaires Comme nous travaillons avec un parallélisme de degré 4 et que les registres sont de 64 bits, la taille des données manipulées sera de 64 4 = 16 bits. Le code assembleur s’écrit inline grâce aux directives __asm__. Les registres MMX se notent %mm0 à %mm7. La syntaxe des cas d’utilisations qui vous seront nécessaires est : — __asm__("movq %mm5, %mm4"); pour les opérations impliquant deux registres MMX déterminés ; — __asm__("movq %0, %%mm4"::"r"(var)); pour mettre dans le registre %mm4 le contenu de la variable var (considérée dans un registre, mettre "m" à la place de "r" si on veut une variable en mémoire) ; — __asm__("movq %%mm2, %0":"=r"(var)); pour mettre dans la variable var (qui est dans un registre) la valeur contenue dans %mm2. Si l’on veut écrire en mémoire, il faut alors mettre "=m" à la place de "=r" ; — une valeur absolue peut être utilisée dans certains cas, il faut alors utiliser la notation $1 pour la constante 1. Si une constante doit être utilisée mais que l’instruction ne peut en prendre, alors il faut passer par un movl. 2 Quelques exemples utiles : — mise à zéro du registre mmx mm0 : __asm__("pxor %mm0, %mm0"); ; — chargement d’une constante 64 bits dans un registre : uint64_t poor = 0xdeadbeef8badf00d; __asm__("movq %0, %%mm3" ::"r"(poor)); — décalage par une constante (ici 8) : __asm__("psrlw $8, %mm3");. Les registres MMX étant ceux du coprocesseur flottant, on peut inspecter leur valeur grâce à i fl dans gdb. Question 2 Chargez les 4 premiers octets du premier macro-bloc Y sous forme de mots de 16 bits dans le registre %mm0. Astuce : utiliser l’instruction punpcklbw. Vous pourrez utiliser les instructions suivantes pour vérifier que tout se passe bien : #ifdef YCrCb_DEBUG /* Ckecing the intermediate results */ uint16_t Y[4]; __asm__("movq %%mm0, %0":"=m"(Y[0])); /* OK! */ #endif Question 3 Chargez les 4 premiers octets des macro-blocs Cr et Cb sous forme de mots de 16 bits et leur soustraire 128. Placer les résultats dans les registres %mm1 et %mm2. Question 4 Effectuez le calcul de 4 composantes R dans les 4 octets de poids faible de %mm3 simultanément à partir de %mm0, %mm1 et des constantes appropriées. Faites de même pour B dans %mm4 puis G dans %mm5. Astuce : utiliser l’instruction packuswb pour se ramener à des octets. Question 5 Construisez 4 mots de 32 bits ayant chacun comme structure 0RGB à partir de %mm3, %mm4 et %mm5. Astuce : l’opération s’apparente à une transposition de matrice 4x4, et peut se faire avec les instructions punpcklbw, punpcklwd et punpckhwd. 3