Vous êtes sur la page 1sur 3

Master CHPS

CHPS0911 – Programmation HPC Avancée sur Cluster


TP2 = OpenMP

Déchiffrer un message encodé


Le programme omp-brute-force.c contient un message chiffré stocké dans le tableau enc[] d'une
longueur de 64 octets. Le message a été chiffré avec une clé de 8 octets à l'aide de l'algorithme DES,
tirant parti des fonctions de bibliothèque déjà installées sur le serveur. La clé de cryptage est une
séquence de 8 caractères ASCII représentant des chiffres numériques, elle est donc comprise entre
«00000000» et «99999999».

Le programme fournit une fonction de déchiffrement (enc, dec, n, clé) pour déchiffrer un message
chiffré à partir de la clé :

• enc est un pointeur vers la zone mémoire contenant le message chiffré ;

• n est la longueur (en octets) du message chiffré ;

• dec est un pointeur vers une zone mémoire de n octets (la même longueur que le message
chiffré), qui doit être pré-allouée par l'appelant, et qui à la fin de l'appel contiendra le
message déchiffré ;

• key est le pointeur vers la clé à utiliser pour déchiffrer; la clé a une longueur de 8 octets.

L'algorithme de décryptage utilisé (Data Encryption Standard, DES) produit toujours un message
décrypté avec n'importe quelle clé ; si la clé n'est pas la bonne, le message déchiffré contiendra des
caractères "absurdes". Dans notre cas, le message en clair est une chaîne de texte (terminée par un \0,
imprimable par printf()), dont les dix premiers caractères sont «0123456789» (sans guillemets).

Écrivez un programme pour effectuer une attaque "force brute" sur l'espace clé, en exploitant le
parallélisme OpenMP. Le programme doit essayer toutes les clés possibles de 00000000 à 99999999,
jusqu'à ce qu'il reçoive un message déchiffré commençant par la séquence «0123456789» ; trouvé la
clé, le programme doit imprimer le message déchiffré, qui est une citation d'un ancien film.

Notez que, en raison des caractéristiques de l'algorithme de chiffrement utilisé, il existe 28 = 256 clés
différentes qui déchiffrent correctement le message ; ceci est dû au fait que l'algorithme ignore le
huitième bit de chaque octet de la clé (la longueur réelle de la clé est donc de 56 bits au lieu de 64).
Pour les besoins de l'exercice, il suffit de trouver l'une des clés de déchiffrement correctes.

Il est conseillé d'utiliser une construction omp parallel en insérant dans quel code affectant un sous-
ensemble approprié de l'espace clé à chaque thread. Cependant, rappelez-vous que la construction
omp parallel s'applique aux blocs structurés, c'est-à-dire aux blocs avec un seul point d'entrée et un
seul point de sortie. Ainsi, un thread qui a trouvé la clé correcte ne peut pas quitter le bloc avec return,
break ou goto car cela devrait provoquer une erreur de compilation. Comme nous ne voulons pas
attendre que tous les threads aient exploré tout l'espace clé pour terminer, vous devez mettre en œuvre
un mécanisme de signalement pour qu'ils arrêtent dès que l'un des threads trouve la clé. Aucune
solution brutale comme exit() ou abort() n'est autorisée pour terminer le programme.

Produit Scalaire de deux arrays


Le fichier omp-dot.c contient une implémentation en série d'un programme qui calcule le produit
scalaire de deux tableaux v1[] et v2[]. Le programme accepte comme seul paramètre d'entrée la
longueur n des tableaux, qui sont initialisés de manière déterministe, afin de connaître leur produit
scalaire sans avoir à le calculer. Rappelons que le produit scalaire de deux tableaux v1[] et v2[] de
longueur n est défini comme :
!"#

! 𝑣1[𝑖] × 𝑣2[𝑖]
$%&

Parallélisez la version série, en utilisant initialement la construction omp parallel avec les clauses que
vous jugez appropriées. Vous pouvez procéder comme suit : soit P le nombre de threads OpenMP, le
programme doit logiquement partitionner le tableau en P blocs de taille approximativement uniforme.
Le p-ième thread (0 ≤ p <P) calcule une partie du produit scalaire correspondant à la somme des
produits des éléments de v1[] et v2[] des indices my_start, ..., my_end-1, soit :
my_end"#

my_p = ! 𝑣1[𝑖] × 𝑣2[𝑖]


$%my_start

Il existe plusieurs façons d'accumuler des résultats partiels. Une possibilité est que la valeur calculée
par le p-ième thread soit stockée dans l'élément partial_p[p], où partial_p[] est un tableau de longueur
P. De cette façon, chaque thread gère un élément différent de partial_p[] et aucune condition de
concurrence ne se produit. Le maître calcule ensuite la somme des valeurs dans partial_p[],
déterminant ainsi le résultat recherché. Faites attention à gérer correctement le cas où la longueur n
des tableaux n'est pas un multiple de P. Une solution plus pratique consiste à utiliser la clause
reduction(), vous pouvez essayer les deux solutions.

Fréquence des caractères


Le fichier omp-letters.c contient une version série d'un programme qui calcule le nombre
d'occurrences et les fréquences des 26 lettres de l'alphabet qui apparaissent dans un fichier lu à partir
de l'entrée standard ; les instructions d'utilisation du programme sont décrites dans les premiers
commentaires. Trois exemples de livres sont fournis au format ASCII mis à disposition par Project
Gutenberg (https://www.gutenberg.org/). Il est intéressant d'observer que les fréquences des
caractères sont très similaires dans les trois œuvres, comme elles le seraient dans tout texte d'une
certaine longueur en langue anglaise. Comme la distribution des fréquences de caractères change pour
chaque langue, vous pouvez expérimenter des livres dans d'autres langues disponibles sur le site Web
du Projet Gutenberg.
Modifiez la fonction make_freq() pour tirer parti du parallélisme OpenMP. Vous pouvez créer un
tableau bidimensionnel local_hist[num_threads][26], où num_threads est la taille du pool de threads
OpenMP. Après avoir initialisé le tableau à zéro, chaque thread incrémente la valeur appropriée de sa
ligne. Enfin, le nombre d'occurrences de chaque caractère est obtenu en ajoutant la colonne local_hist
appropriée. N'oubliez pas que la fonction make_hist() doit renvoyer le nombre total de lettres de
l'alphabet trouvé.

Simuler la clause "schedule(dynamic)" de OpenMP


Nous avons vu en cours comment utiliser la clause schedule (dynamic) de la construction omp
parallel for pour attribuer dynamiquement chaque itération d'une boucle for au premier thread
OpenMP disponible. Le but de cet exercice est de simuler la clause schedule(dynamic) en utilisant
uniquement la construction omp parallel (et non omp parallel for).
Le fichier omp-dynamic.c contient une implémentation en série d'un programme qui effectue les
calculs suivants : Le programme crée et initialise un tableau vin[] de n entiers (la valeur n peut être
passée depuis la ligne de commande; si rien n'est spécifié, une valeur par défaut est utilisée). Le
programme crée un second tableau vout[], toujours de longueur n, et définit son contenu de telle
manière que nous avons vout[i] = Fib(vin[i])), étant Fib(k) le k-ième nombre de la séquence de
Fibonacci (Fib(0) = Fib(1) = 1; Fib(k) = Fib(k - 1) + Fib(k - 2) si k ≥ 2). Le calcul des nombres de
Fibonacci se fait de manière volontairement inefficace en utilisant un algorithme récursif, cela sert à
garantir que le temps de calcul de vout[i] varie considérablement lorsque i varie.
Commencez par paralléliser la boucle for indiquée dans le code avec le commentaire [TODO] en
utilisant la construction omp parallel for. En gardant la longueur n des tableaux fixe, observez les
temps d'exécution dans les cas suivants :
1. Paralléliser la boucle avec la directive #pragma omp parallel for, qui utilise l'ordonnanceur
par défaut (qui dans le cas de GCC est l'ordonnanceur statique de taille de bloc n /
number_thread_OpenMP) ;
2. Paralléliser le cycle avec un programme statique mais avec une taille de bloc plus petite, par
exemple 64. Vous pouvez utiliser la directive #pragma omp parallel pour le schedule(static,
64);
3. Paralléliser la boucle avec un schedule dynamique, en utilisant la directive #pragma omp
parallel for schedule (dynamic). Rappelez-vous que dans ce cas, la taille de bloc par défaut
est 1.
Une fois que cela est fait, il vous est demandé de réproduire le même comportement qu'au point 3
(schedule dynamique avec bloc de taille 1) en utilisant la construction omp parallel (pas omp parallel
for). Suggestion : créez un pool de threads OpenMP et utilisez une variable partagée pour indiquer
quel est l'index de l'élément suivant de vin[] qui n'a pas encore été traité. Chaque thread acquiert de
manière atomique (en utilisant les directives OpenMP appropriées) l'élément suivant de vin[], le cas
échéant, et le traite en parallèle.

MergeSort
Le fichier omp-mergesort.c contient une implémentation récursive de l'algorithme MergeSort (des
détails de l'algorithme se trouvent dans les commentaires du code source).
Paralléliser le programme à l'aide des tâches OpenMP, en vous assurant que chaque appel récursif
est délégué à une tâche OpenMP. Vérifiez si la version parallèle est plus rapide ou plus lente que la
version série en faisant varier le nombre de threads OpenMP. Utilisez une longueur appropriée pour
le tableau, afin d'obtenir des temps d'exécution pas trop courts (risque de variabilité excessive).