Commit a48d1412 authored by Louis Lerbourg's avatar Louis Lerbourg

done

parent 9193b51c
......@@ -35,7 +35,7 @@
Utilisation des extensions SSE du x86\_64 pour le calcul\\
de la conversion YCbCr vers RGB en Motion Jpeg\\~\\
\large
Frédéric Pétrot\\
Frédéric Pétrot/Arthur Perais\\
Durée~: 3 heures
\end{center}
......@@ -46,40 +46,64 @@ Le sujet est découpé en parties qui peuvent au départ être faites plus ou mo
%Merci de m'envoyer votre fichier \texttt{conv.c} au plus tard lundi soir minuit, c'est là dessus que vous serez notés (ou pas, ...).
\section{Introduction}
Ce TP a pour objectif de mettre en pratique l'utilisation des opérations SIMD du processeur x86\_64.
Ce TP a pour objectif de mettre en pratique l'utilisation des opérations SIMD du processeur x86\_64. Ces opérations ont été ajoutées au fil du temps via de nombreuses extensions aux jeux d'instruction.
SIMD signifie \emph{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.
Nous utiliserons uniquement les extensions dites «~SSE~» (SSE, SSE2,SSE3, SSSE3, SSE4.1 et SSE4.2), sachant qu'il en existe de plus modernes (AVX, AVX2, AVX512) et de plus anciennes (MMX), ...
Il y a 396 instructions SSEx (rien que ça), mais vous serez guidés pour sélectionner celles dont vous aurez l'usage.
Il y a 396 instructions SSEx (rien que ça), mais vous serez -- un peu -- guidés pour sélectionner celles dont vous aurez l'usage.
Les instructions SSE d'Intel travaillent sur des registres de 128 bits, qui peuvent contenir 2 doubles \lstinline{(double)}, 4 flottants \lstinline{(float)}, 4 entiers de 32 bits \lstinline{(int32_t)}, 8 entiers de 16 bits \lstinline{(int16_t)}, ou 16 entiers de 8 bits \lstinline{(int8_t)}.
Les instructions SSE d'Intel travaillent sur des registres de 128 bits, qui peuvent contenir 2 doubles \lstinline{(double)}, 4 flottants \lstinline{(float)}, 4 entiers de 32 bits \lstinline{(int32_t)}, 8 entiers de 16 bits \lstinline{(int16_t)}, ou 16 entiers de 8 bits \lstinline{(int8_t)}. Dans le cas des entiers, certaines instructions interprètent les entiers comme des entiers signés, alors que d'autres les interprètent comme dans entiers non-signés.
Afin de simplifier l'écriture du code dans du C, vous utiliserez les fonctions dites «~intrinsèques~» qui permettent d'utiliser des instructions assembleur plus simplement que l'\lstinline{inline asm}.
\lstinline{Gcc} donne accès à ces fonctions en incluant le fichier \lstinline{x86intrin.h} (c'est fait dans les fichiers à trous que je vous fournis), et elles commencent toutes par \lstinline{__mm__}.
\lstinline{Gcc} fournit également trois types~: \lstinline{__m128d} pour 2 doubles 64 bits, \lstinline{__m128} pour 4 flottants 32 bits, \lstinline{__m128i} pour tous les types entiers, donc c'est au programmeur de savoir ce qu'il contient en réalité.
\lstinline{Gcc} donne accès à ces fonctions en incluant le fichier \lstinline{x86intrin.h} (c'est fait dans les fichiers à trous que je vous fournis), et elles commencent toutes par \lstinline{_mm_}.
\lstinline{Gcc} fournit également trois types~: \lstinline{__m128d} pour 2 doubles 64 bits, \lstinline{__m128} pour 4 flottants 32 bits, \lstinline{__m128i} pour tous les types entiers, donc c'est au programmeur de savoir ce qu'il contient en réalité. De manière générale, on peut s'attarder sur le suffixe de chaque intrinsèque pour déterminer sur quelle type de donnée on opère : \\
\begin{center}
\begin{tabular}{lll}
\multicolumn{1}{c}{Type} &
\multicolumn{1}{c}{INT} &
\multicolumn{1}{c}{FP} \\ \hline
SISD &
Scalaire entier = instructions classiques &
\begin{tabular}[c]{@{}l@{}}"Scalar Single Precision (1x32b)" (\textbf{\_ss}) \\ "Scalar Double Precision (1x64b)" \textbf{(\_sd})\end{tabular} \\
SIMD &
\begin{tabular}[c]{@{}l@{}}
"Packed Integer" (\textbf{\_epi8/16/32/64} ou \textbf{\_si}) \\
"Packed Unsigned Integer" \textbf{(\_epu8/16/32/64)} \end{tabular} &
\begin{tabular}[c]{@{}l@{}}"Packed Single Precision (4x32b)" (\textbf{\_ps})\\ "Packed Double Precision (2x64b)" (\textbf{\_pd})\end{tabular}
\end{tabular}
\end{center}
\newpage
En pratique, j'ai utilisé, pour l'implantation que j'ai réalisée, uniquement les types \lstinline{__m128} et \lstinline{__m128i} et les 9 fonctions intrinsèques suivantes~: \\
En pratique, j'ai utilisé, pour l'implantation que j'ai réalisée, uniquement les types \lstinline{__m128} et \lstinline{__m128i} et les 9 fonctions intrinsèques suivantes~:\\
\footnotesize
\begin{tabular} {lll}
\texttt{\_mm\_set\_ps1} & \texttt{\_mm\_store\_si128} & \texttt{\_mm\_unpacklo\_epi16}\\
\texttt{\_mm\_unpacklo\_epi8} & \texttt{\_mm\_packus\_epi16} & \texttt{\_mm\_packus\_epi32}\\
\texttt{\_mm\_cvtps\_epi32} & \texttt{\_mm\_set1\_epi32} & \texttt{\_mm\_setr\_ps}
\end{tabular}
\normalsize
Le détail du comportement de ces instructions est disponible sur le site d'Intel à l'url suivante (indispensable de l'avoir ouverte pendant le TP~!):\\
{\footnotesize\ttfamily \url{https://software.intel.com/sites/landingpage/IntrinsicsGuide/#expand&techs=SSE,SSE2,SSE3,SSSE3,SSE4_1,SSE4_2}}.
\vspace{10pt}
Il est cependant possible d'implémenter l'algorithme en utilisant d'autres instructions, la liste n'est donc ni exhaustive ni contraignante. Le détail du comportement de ces instructions est disponible sur le site d'Intel à l'url suivante (indispensable de l'avoir ouverte pendant le TP~!):\\
{\footnotesize\ttfamily \url{https://software.intel.com/sites/landingpage/IntrinsicsGuide/#expand&techs=SSE,SSE2,SSE3,SSSE3,SSE4_1,SSE4_2}}. On se limitera cependant à SSE/SSSE, donc au SIMD 128-bit (pas d'AVX, sauf si vous voulez faire une version 256-bit après avoir fait la version 128-bit).
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, bleu pour l'affichage d'une image dans un \emph{frame buffer}.
\section{Travail demandé}
\subsection{Préliminaires}
Récupérez sur le site du cours (\lstinline{https://ensiwiki.ensimag.fr/images/d/d1/Tp2_src.tgz}) l'archive contenant les sources nécessaires au TP.
Cette archive s'expanse dans le répertoire \verb+tp2_src/+, et contient une vidéo \verb+ice_age_256x144_444.mjpeg+ qui sera notre benchmark, un \verb+Makefile+, des fichiers objets contenant les différentes phases du décodage hormis la conversion YUV vers RGB, et des fichiers sources contenant diverses implantations (dont certaines partielles) de la conversion.
On clonera le repo git à partir du gitlab (\lstinline{git clone https://gitlab.ensimag.fr/petrotf/3a-archi.git}). Si vous comptez travailler sur votre machine, il conviendra probablement d'installer la librarie SDL afin d'afficher ce que l'on décode à l'écran. Sur Ubuntu : \\
\lstinline{sudo apt install libsdl2-2.0-0 libsdl2-gfx-1.0-0 libsdl2-image-2.0-0 libsdl2-mixer-2.0-0 libsdl2-net-2.0-0 libsdl2-ttf-2.0-0 libsdl2-dev} \\
On travaillera dans le répertoire \lstinline{3a-archi/TP/tp-simd/tp2_src_etd}, qui contient une vidéo \lstinline{Ice_age_256x144_444.mjpeg} qui sera notre benchmark, un \lstinline{Makefile}, des fichiers objets contenant les différentes phases du décodage hormis la conversion YUV vers RGB, et des fichiers sources contenant diverses implantations (dont certaines partielles) de la conversion.
\begin{description}
\item[\texttt{conv-float.c}]~\\
la version initiale en virgule flottante~;
......@@ -127,7 +151,9 @@ Plus fort, il existe des instructions qui font ces conversions en effectuant des
\subsection*{Question 2}
Ouvrez le fichier \lstinline[language=bash]{conv-sse-a-trou.c} et suivez les consignes qui s'y trouvent.
La mauvaise nouvelle est qu'il n'est pas possible de changer par étapes successives le code, car les données sont dans des registres spéciaux avec des formats spéciaux, et que la conversion vers les formats du C n'est pas immédiate (\emph{c.f.} l'implantation de la fonction \lstinline{p128_x}).
Donc soit gdb est votre ami (qui affiche les \lstinline{__mm128} selon toutes les configurations possibles), soit vous utilisez la fonction \lstinline{p128_x} qui permet d'afficher le contenu d'un registre (que vous pouvez comparer à ce que vous obtenez lorsque vous exécutez la version initiale, c'est ainsi que j'ai débuggé).
Donc soit gdb est votre ami (qui affiche les \lstinline{__mm128} selon toutes les configurations possibles), soit vous utilisez la fonction \lstinline{p128_x} qui permet d'afficher le contenu d'un registre (que vous pouvez comparer à ce que vous obtenez lorsque vous exécutez la version initiale, c'est ainsi que j'ai débuggé). A noter : les variables sont données à titre indicatif et vous avez le droit de ne pas toute les utiliser ou d'en utiliser plus.
Une dernière chose, la commande \lstinline{objdump -d executable | less} vous affichera les instructions du binaire (il suffit alors de chercher la bonne fonction). Utile si vous voulez voir ce que donne la traduction C vers assembleur avec instructions SSE.
\subsection*{Question 3}
Compilez votre version et les différentes versions fournies, décodez le film full patate, et faites un graphe du temps de décodage des différentes implantations.
......
SHELL=/bin/bash
CFLAGS=-O3 -Wall -march=core2 # pcserveur est un sandybridge, vérifier sur les machines de la salle de TP
CFLAGS=-O3 -Wall -march=native # native = on cible la machine sur laquelle on compile. Attention cependant, certaines vieilles machines n'auront peut-être pas toutes les extensions que vous souhaitez utiliser
CONV=conv-int
OBJS=huffman.o idct.o iqzz.o main.o screen.o skip_segment.o unpack_block.o upsampler.o
......@@ -9,19 +9,19 @@ all :
@gawk -F : '/^mjpeg/{print "make", $$1}' Makefile
mjpeg-float : conv-float.o $(OBJS)
gcc -Bstatic -o $@ $^ -lSDL
gcc -no-pie -Bstatic -o $@ $^ -lSDL
mjpeg-int : conv-int.o $(OBJS)
gcc -Bstatic -o $@ $^ -lSDL
gcc -no-pie -Bstatic -o $@ $^ -lSDL
mjpeg-mmx : conv-mmx.o $(OBJS)
gcc $(CFLAGS) -Bstatic -o $@ $^ -lSDL
gcc -no-pie $(CFLAGS) -Bstatic -o $@ $^ -lSDL
mjpeg-conv-unrolled4-float-a-trou : conv-unrolled4-float-a-trou.o $(OBJS)
gcc $(CFLAGS) -Bstatic -o $@ $^ -lSDL
gcc -no-pie $(CFLAGS) -Bstatic -o $@ $^ -lSDL
mjpeg-conv-sse-a-trou : conv-sse-a-trou.c $(OBJS)
gcc $(CFLAGS) -Bstatic -o $@ $^ -lSDL
gcc -no-pie $(CFLAGS) -Bstatic -o $@ $^ -lSDL
realclean : clean
rm -f mjpeg-float mjpeg-int mjpeg-mmx mjpeg-unrolled4-float-a-trou mjpeg-conv-sse-a-trou
......
......@@ -66,33 +66,33 @@ void YCrCb_to_ARGB(uint8_t *YCrCb_MCU[3], uint32_t *RGB_MCU, uint32_t nb_MCU_H,
* Instructions à utiliser
* _mm_set...
*/
const __m128i v0 = _mm_...(0);
const __m128i v0 = _mm_set1_epi32(0);
MCU_Y = YCrCb_MCU[0];
MCU_Cb = YCrCb_MCU[1];
MCU_Cr = YCrCb_MCU[2];
for (i = 0; i < 8 * nb_MCU_V; i++) {
for (j = 0; j < 8 * nb_MCU_H; /* TODO: a ajuster, ... */) {
for (j = 0; j < 8 * nb_MCU_H; j+=4) {
/* On travaille à présent sur des vecteurs de 4 éléments d'un coup */
index = i * (8 * nb_MCU_H) + ...;
index = i * (8 * nb_MCU_H) + j;
/*
* Chargement des macro-blocs:
* instruction à utiliser
* _mm_set...
*/
Y = _mm_...(MCU_Y [index + ...], ..., ..., ...);
Cb = _mm_...(MCU_Cb[index + ...], ..., ..., ...);
Cr = _mm_...(MCU_Cr[index + ...], ..., ..., ...);
Y = _mm_setr_ps(MCU_Y[index + 0],MCU_Y[index + 1] , MCU_Y[index + 2],MCU_Y[index + 3]);
Cb = _mm_setr_ps(MCU_Cb[index + 0],MCU_Cb[index + 1] , MCU_Cb[index + 2],MCU_Cb[index + 3]);
Cr = _mm_setr_ps(MCU_Cr[index + 0], MCU_Cr[index + 1], MCU_Cr[index + 2], MCU_Cr[index + 3]);
/*
* Calcul de la conversion YCrCb vers RGB:
* aucune instruction sse explicite
*/
Rf = ...;
Bf = ...;
Gf = ...;
Rf = (Cr - 128) * 1.402f + Y;
Bf = (Cb - 128) * 1.7772f + Y;
Gf = Y - (Cb - 128) * 0.381834f - (Cr - 128) * 0.71414f;
/*
* Conversion en entier avec calcul de la saturation:
......@@ -104,34 +104,36 @@ void YCrCb_to_ARGB(uint8_t *YCrCb_MCU[3], uint32_t *RGB_MCU, uint32_t nb_MCU_H,
* _mm_cvt...
* _mm_pack...
*/
R = ...
G = ...
B = ...
R = _mm_cvtps_epi32(Rf);
G = _mm_cvtps_epi32(Gf);
B = _mm_cvtps_epi32(Bf);
Rs = ...
Gs = ...
Bs = ...
Rs = _mm_packus_epi32(R, v0);
Gs = _mm_packus_epi32(G, v0);
Bs = _mm_packus_epi32(B, v0);
Rc = ...
Gc = ...
Bc = ...
Rc = _mm_packus_epi16(Rs, v0);
Gc = _mm_packus_epi16(Gs, v0);
Bc = _mm_packus_epi16(Bs, v0);
/*
* Transposition de la matrice d'octets, brillant !
* instructions à utiliser
* _mm_unpack...
Rc = 00000000000000000000|RC1|Rc2|Rc3|Rc4
ARGB = |00|R4|G4|B4|00|R3|G3|B3|00|R2|G2|B2|00|R1|G1|B1
*/
__m128i tmp0, tmp1;
tmp0 = ...
tmp1 = ...
ARGB = ...
tmp0 = _mm_unpacklo_epi8(Bc, Gc);
tmp1 = _mm_unpacklo_epi8(Rc, v0);
ARGB = _mm_unpacklo_epi16(tmp0, tmp1);
/*
* Écriture du résultat en mémoire
* instruction à utiliser
* _mm_store...
*/
_mm_store...
_mm_store_si128((__m128i*)(RGB_MCU + i * (8 * nb_MCU_H) + j), ARGB);
}
}
}
......@@ -27,24 +27,73 @@
#include "utils.h"
/*
* TODO: Implantation déroulant la boucle 4 fois
* Implantation déroulant la boucle 4 fois
*/
void YCrCb_to_ARGB(uint8_t *YCrCb_MCU[3], uint32_t *RGB_MCU, uint32_t nb_MCU_H, uint32_t nb_MCU_V)
{
uint8_t *MCU_Y, *MCU_Cr, *MCU_Cb;
uint8_t index, i, j;
/* TODO: vos déclarations de variables ici */
... /* FIXME: pour être certain que ça ne compile pas :) */
int32_t R[4], G[4], B[4];
uint32_t ARGB[4];
MCU_Y = YCrCb_MCU[0];
MCU_Cr = YCrCb_MCU[2];
MCU_Cb = YCrCb_MCU[1];
for (i = 0; i < 8 * nb_MCU_V; i++) {
for (j = 0; j < 8 * nb_MCU_H; /* TODO: a ajuster, ... */) {
for (j = 0; j < 8 * nb_MCU_H; j+=4) {
/* On travaille à présent sur des vecteurs de 4 éléments d'un coup */
/* TODO: juste fait le ! */
index = i * (8 * nb_MCU_H) + ...;
index = i * (8 * nb_MCU_H) + j;
/* Calcul de la conversion pixel par pixel */
R[0] = (MCU_Cr[index] - 128) * 1.402f + MCU_Y[index];
R[1] = (MCU_Cr[index+1] - 128) * 1.402f + MCU_Y[index+1];
R[2] = (MCU_Cr[index+2] - 128) * 1.402f + MCU_Y[index+2];
R[3] = (MCU_Cr[index+3] - 128) * 1.402f + MCU_Y[index+3];
B[0] = (MCU_Cb[index] - 128) * 1.7772f + MCU_Y[index];
B[1] = (MCU_Cb[index+1] - 128) * 1.7772f + MCU_Y[index+1];
B[2] = (MCU_Cb[index+2] - 128) * 1.7772f + MCU_Y[index+2];
B[3] = (MCU_Cb[index+3] - 128) * 1.7772f + MCU_Y[index+3];
G[0] = MCU_Y[index] - (MCU_Cb[index] - 128) * 0.381834f - (MCU_Cr[index] - 128) * 0.71414f;
G[1] = MCU_Y[index+1] - (MCU_Cb[index+1] - 128) * 0.381834f - (MCU_Cr[index+1] - 128) * 0.71414f;
G[2] = MCU_Y[index+2] - (MCU_Cb[index+2] - 128) * 0.381834f - (MCU_Cr[index+2] - 128) * 0.71414f;
G[3] = MCU_Y[index+3] - (MCU_Cb[index+3] - 128) * 0.381834f - (MCU_Cr[index+3] - 128) * 0.71414f;
/* Saturation des valeurs pour se ramener à 32 bits pour Alpha, Red, Green, Blue */
if (R[0] > 255) R[0] = 255;
if (R[1] > 255) R[1] = 255;
if (R[2] > 255) R[2] = 255;
if (R[3] > 255) R[3] = 255;
if (R[0] < 0) R[0] = 0;
if (R[1] < 0) R[1] = 0;
if (R[2] < 0) R[2] = 0;
if (R[3] < 0) R[3] = 0;
if (G[0] > 255) G[0] = 255;
if (G[1] > 255) G[1] = 255;
if (G[2] > 255) G[2] = 255;
if (G[3] > 255) G[3] = 255;
if (G[0] < 0) G[0] = 0;
if (G[1] < 0) G[1] = 0;
if (G[2] < 0) G[2] = 0;
if (G[3] < 0) G[3] = 0;
if (B[0] > 255) B[0] = 255;
if (B[1] > 255) B[1] = 255;
if (B[2] > 255) B[2] = 255;
if (B[3] > 255) B[3] = 255;
if (B[0] < 0) B[0] = 0;
if (B[1] < 0) B[1] = 0;
if (B[2] < 0) B[2] = 0;
if (B[3] < 0) B[3] = 0;
ARGB[0] = ((R[0] & 0xFF) << 16) | ((G[0] & 0xFF) << 8) | (B[0] & 0xFF);
ARGB[1] = ((R[1] & 0xFF) << 16) | ((G[1] & 0xFF) << 8) | (B[1] & 0xFF);
ARGB[2] = ((R[2] & 0xFF) << 16) | ((G[2] & 0xFF) << 8) | (B[2] & 0xFF);
ARGB[3] = ((R[3] & 0xFF) << 16) | ((G[3] & 0xFF) << 8) | (B[3] & 0xFF);
/* Écriture du pixel dans le macro-bloc de sortie */
RGB_MCU[(i * (8 * nb_MCU_H) + j)] = ARGB[0];
RGB_MCU[(i * (8 * nb_MCU_H) + j + 1)] = ARGB[1];
RGB_MCU[(i * (8 * nb_MCU_H) + j + 2)] = ARGB[2];
RGB_MCU[(i * (8 * nb_MCU_H) + j + 3)] = ARGB[3];
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment