• Aucun résultat trouvé

Nous quittons le domaine de l’optimisation numérique et de l’algorithmique pour celui de la programmation, qui traite de l’implémentation logicielle performante des concepts rencontrés dans ce chapitre. Nous détaillons différentes méthodes pour optimiser la vitesse de traitement d’un algorithme donné (le décodage de type "Message Passing") par un processeur d’ordinateur. Cette section vise donc à augmenter la vitesse du décodage, sans faire varier son efficacité.

La caractéristique la plus spécifique d’un code LDPC est la représentation de sa matrice de "parity check" dans la mémoire de l’ordinateur. En effet, les nombreux "parity checks" font l’efficacité remarquable du décodage LDPC. Il est bien entendu hors de question d’enregistrer chacun des éléments de la matrice, car cette matrice est trop importante.

13.6 Réduction du temps de calcul 161

Au contraire, nous tirons parti de la faible densité de cette matrice en ne mémorisant que les emplacements des éléments 1 de cette matrice, ce qui donne une représentation sous forme de graphe que nous avons déjà rencontrée. Le problème de la représentation en mémoire de cet arbre répond aux multiples façons de considérer cet arbre : doit-on mémoriser à quels nœuds de variable chaque nœud de parité est connecté plutôt que l’inverse, ou doit-on plutôt considérer chaque connexion et mémoriser les deux nœuds qu’elle relie ? Enfin, comment doit- on mémoriser les différents LLR mis en jeu dans le décodage LDPC (voir section précédente) ? Cette question est d’importance car chaque étape élémentaire du décodage LDPC demande d’aller chercher en mémoire un ou plusieurs LLR. En fonctions des machines, ces accès mémoire peuvent consommer plusieurs cycles pendant lesquels le processeur est inactif. Enfin, certaines machines possèdent une mémoire cache à accès rapide, mais souvent en trop petite quantité pour enregistrer notre arbre entier.

Nous avons empiriquement opté pour la représentation suivante : • Un tableau regroupe les LLR associés à chaque nœud de variable.

• Pour chaque nœud de parité, on alloue une structure de données contenant un tableau regroupant les LLR associés à chaque connexion dans laquelle le nœud de parité est impliqué, et un tableau de pointeurs3 vers les éléments du tableau des LLR des nœuds de variable avec lesquels est associé le nœud de parité.

• Un tableau regroupant les indices (positions) des nœuds de variable connectés au nœud de parité. Cette information n’est pas nécessaire au décodage, mais peut être utile, par exemple pour vérifier la validité d’un mot code.

Chaque étape de l’algorithme est optimisée pour éviter les calculs redondants et choisir les opérations élémentaires les plus rapides. Cette optimisation est réalisée à l’aide d’un logiciel de profilage4, capable d’annoter chaque ligne de code par le nombre d’appels et le temps total d’exécution. Nous avons ainsi pu détecter certaines lignes de code impliquées dans une structure de boucle, et dont le résultat reste constant pour toute itération de la boucle. Ces lignes de code redondantes sont alors factorisées, c’est-à-dire sorties de la boucle. Compte tenu de la taille de notre arbre, et par conséquent du nombre d’itérations important de chaque boucle, la factorisation de code est une optimisation très fructueuse. Elle nous a permis de gagner plusieurs ordres de grandeur sur le temps d’exécution, aussi bien de l’algorithme LDPC lui même, que sur le calcul d’intégrales gaussiennes préalable au décodage multi-niveaux, ou sur le transfert des LLR entre les niveaux. Parfois, il est nécessaire d’ordonner de façon astucieuse plusieurs boucles imbriquées afin d’exhiber la factorisation.

Au cœur de l’algorithme "Message Passing", une poignée de lignes de code réalisant le calcul de l’équation 12.16 se trouvent au centre de nombreuses boucles imbriquées. L’effort d’optimisation doit donc principalement se porter sur ces lignes5. Pour ces lignes de code, nous choisissons donc les opérations les plus rapides : instructions conditionnelles plutôt que multiplication par −1. . .

3Un pointeur est une adresse mémoire, qui nous permet de désigner une variable déjà définie. Ce sont ces

pointeurs qui portent l’information de la structure du graphe.

4

Valgrind, disponible à l’adresse http://www.valgrind.org

5Cet effet général est toujours surprenant, même pour l’initié : gagner 90% au prix de larges efforts sur une

fraction ne représentant que 10% du tout, n’aboutit qu’à une optimisation de quelques pour cents. Cet effet est davantage marqué en algorithmique où quelques lignes de code consomment la plupart du temps d’exécution.

162 Chapitre 13 : Réalisation et optimisation de la réconciliation 0 1 2 3 4 5 0 1 2 3 4 5 2 2.05 2.1 2.15 0.24 0.26

Fig. 13.7: Fonction φ(x) = − ln(tanh(x/2)). Cette fonction est appelée au centre de toutes les boucles du décodage LDPC. C’est pour cela que nous devons choisir une méthode d’évaluation optimale pour cette fonction. Notons que cette fonction est son propre inverse : φ(φ(x)) = x. La figure de droite montre l’approximation que nous avons utilisé pour calculer rapidement la fonction φ.

L’algorithme "Message Passing" demande de nombreux appels à la fonction φ(x) = − ln(tanh(x/2)), représentée figure 13.7. Nous disposons de plusieurs méthodes pour calculer cette fonction :

• utiliser les fonctions mathématiques standard ln et tanh. Cette méthode n’est pas optimale car elle nécessite deux appels à des fonctions non linéaires programmées avec une précision de 16 ou 32 bits, de [0; ∞[ vers [0; ∞[.

• approximer la fonction φ par une fonction linéaire par morceaux. Nous avons découpé l’intervalle [0; 10] contenant les valeurs significatives de φ en huit intervalles, sur lesquels nous définissons la meilleure approximation linéaire de φ. La détermination de l’intervalle à partir de la valeur d’abscisse se fait par dichotomie. Cette méthode a le désavantage d’approximer parfois grossièrement φ, et de parcourir plusieurs instructions condition- nelles pour trouver l’intervalle dans lequel se situe une valeur x donnée.

• calculer à l’avance un tableau de valeurs pour φ, dont l’index de l’entrée est une fonction linéaire de l’abcisse x. Ce tableau définit une fonction en forme d’escalier approximant φ. Nous avons empiriquement choisi de coder l’intervalle [0 : 8] sur 12 bits, en calculant un tableau de 4 096 entrées. Cette méthode a le désavantage de nécessiter un grand espace mémoire, et les accès associés, pour stocker les valeurs pré-calculées. Aussi, l’intervalle de binarisation est de largeur constante, alors que la fonction φ varie rapidement pour des valeurs proches de 0.

À titre de comparaison, la première méthode calcule 106 valeurs de φ en 4 secondes (sur un ordinateur de type Pentium 4, 2,4 GHz), alors que les deux dernières méthodes mettent envi- ron 0, 4 secondes. Cependant, la mauvaise qualité de l’approximation fournie par la deuxième méthode dégrade l’efficacité de l’algorithme de décodage. Nous avons dont finalement choisi la dernière méthode car, à vitesse égale, elle offre une approximation plus exacte de φ. La précision de cette approcimation est illustrée par la figure 13.7.

Les objets manipulés par l’algorithme "Message Passing" – les LLR – sont des grandeurs réelles. Nous pouvons améliorer la vitesse de décodage en utilisant une représentation à virgule fixe des nombres réels. Les architectures de processeurs ren- contrées dans les stations de travail utilisent une représentation des nombres réels à virgule