RENPAR'14 / ASF / SYMPA Hamamet, Tunisie, 10 - 13 avril 2002 Adaptation du calcul du CRC à une architecture parallèle à instructions longues∗ Jean--Didier Legat3 Damien Hubaux1, Luc Vandendorpe 2 et Jean 1 CETIC asbl rue Clément Ader, 8 6041 Gosselies - Belgique [email protected] 2 UCL - TELE place du Levant, 2 1348 Louvain-la-Neuve - Belgique [email protected] 3 UCL - DICE place du Levant, 3 1348 Louvain-la-Neuve - Belgique [email protected] Les codes de redondance cyclique (CRC) sont beaucoup utilisés pour la détection d'erreurs. Des méthodes ont été développées pour obtenir des performances optimales tant en matériel qu'en logiciel. Mais dans le cas du logiciel, la méthode classique ne parvient pas toujours à tirer pleinement avantage d'un processeur à instructions longues (VLIW), particulièrement s'il y a des cycles de délai lors de la lecture en mémoire. La méthode présentée dans cet article permet d'utiliser pleinement le potentiel d'un tel processeur. Ce calcul s'effectue dès lors au moins quatre fois plus vite. processeur de traitement du signal, instructions longues, code de redondance cyclique 1. Introduction Les Codes de Redondance Cyclique sont des codes de détection d'erreurs très répandus. Le CRC ajoute très peu d'information tout en garantissant une haute probabilité de détection des erreurs. Un CRC de n-k bits peut être décrit comme « le reste de la division, par un polynôme générateur donné G(x), du polynôme des données M(x) multiplié par xn-k », ce qu’on peut écrire synthétiquement: crc(x) = M(x) xn-k modulo G(x) [2]. Cela appelle quelques explications. On travaille avec des polynômes dont les coefficients sont binaires. Les bits de la séquence binaire à vérifier sont considérés comme les coefficients d’un polynôme, la position dans la séquence représentant le degré. Par exemple, la séquence 1011 représente le polynôme x3+x+1. Ces polynômes possèdent une arithmétique propre (Corps de Galois d’ordre 2). L’addition, équivalente à la soustraction, se fait modulo 2 et il n’y a pas de report (x2+x2=0 et pas x3). La relation d’ordre se limite au bit le plus significatif: on ne peut pas dire que 1000 est plus petit que 1011. On peut encore ajouter que le fait de multiplier par xn revient à décaler de n positions (vers la gauche ici). La figure 1 présente une telle division (présentation similaire à une division euclidienne). Division de (x7+x5+x3+x1).x8 (1010101000000000) Par le polynôme générateur x8+x4+x3+x2+1 (100011101) Reste x6+x5+x4+x+1 (1110011) ∗ 1010101000000000 100011101 10010010 100011101 1110010 100011101 11010101 100011101 10110111 100011101 1110011 Fig. 1 – Division d’un polynôme à coefficients binaires La notation habituelle utilise k pour désigner le nombre de bits de données. La taille du message avec l’ajout du CRC est n. La taille du CRC est donc n-k, qui est aussi le degré du polynôme générateur. Dans la description théorique de l’algorithme, on multiplie le message par n-k, c’est à dire qu’on y ajoute n-k zéros: on l’appelle alors le message augmenté. On peut mettre en œuvre cette division bit-à-bit simplement à l'aide de registres à décalages et de portes XOR (OU exclusif); ce circuit porte le nom de « Linear Feedback Shift Register ». Lors de l’introduction des zéros du message augmenté, la valeur du CRC sera lue en sortie. Fig. 2 – Calcul d'un CRC par registres à décalage Les premières implémentations logicielles émulaient ce fonctionnement. Rapidement, une méthode a été développée afin d'utiliser efficacement un processeur [3], c’est-à-dire traiter des mots et non des bits (soit le nombre de bits traités en parallèle). Celle-ci se base sur le fait que le contenu des registres est une certaine combinaison de leur contenu précédent et des nouveaux bits introduits. Pour traiter plusieurs bits en parallèle, il faut pré-calculer ce résultat pour chaque combinaison possible de ces bits: cela nécessite une table de 2 mots de (n-k) bits de long. Pour chaque valeur possible, un élément de la table donne le reste de la division de bits augmentés de n-k zéros. L'algorithme prend la forme suivante, lorsque <n-k (itération sur i, data[i] est large de ! ": crc = ((crc<< )^data[i]) ^ table[crc>>(n-k- )]. Après les bits de données, il faut encore traiter (n-k) zéros (pour former les données augmentées). Généralement, on traite 8 bits à la fois, ce qui nécessite une table de taille raisonnable, et les opérations se font maintenant généralement sur 32 bits (ce qui demande moins d'opérations que signalé dans des articles précédents [4]). On peut noter qu'il existe une variante de cet algorithme qui évite de devoir traiter les bits de zéros en fin de calcul; tout ce qui est dit ici s'applique également dans ce cas. On peut trouver introduction claire et intuitive aux CRC dans [5]. 2. Cas d'un processeur à instructions longues L'algorithme classique du CRC a été mis en œuvre sur un processeur de signal de TI (Texas Instruments) de la famille TMS320C62 [6]. Ce processeur est destiné aux applications exigeantes en calculs tels les systèmes de télécommunication à haut débit. La caractéristique principale de celui-ci est son architecture VLIW (Very Long Instruction Word) qui permet d'exécuter jusqu'à 8 instructions élémentaires en parallèle. Il est conçu pour atteindre des performances maximales en utilisant un pipeline logiciel [7]: chaque itération d'une boucle est démarrée aussitôt que l'unité appropriée est libre. Le noyau de la boucle pipelinée exécute donc plusieurs instructions en parallèle pour le compte d'itérations successives. Signalons d’abord que bien qu’une instruction de lecture en mémoire puisse être démarrée à chaque cycle, le résultat n’est disponible qu’après 5 cycles, à cause de la structure en pipeline de l'architecture C6000. Cette caractéristique n’est pas limitée à ce processeur. Par exemple, le DSP Trimedia de Philips possède également un noyau VLIW avec 5 unités d’exécution. Les instructions d’accès en mémoire y ont un délai de 3 cycles. # $ %&'( Etudions d'abord le cas d'un CRC de 8 bits, en traitant 8 bits à la fois. La formule précédente se simplifie alors: crc = data[i]^table[crc]. Dans la formule présentée, il apparaît clairement que chaque itération utilise le résultat de la précédente. Il est donc impossible de lire anticipativement les valeurs suivantes de la table en mémoire. A chaque itération, le processeur est donc bloqué durant 4 cycles en attendant la valeur précalculée et aucun parallélisme n'est possible. 3. Algorithme modifié Le but est d'éviter les 4 cycles d'attente, qui sont d'autant plus pénalisants que les 8 unités d'exécution sont inutilisées. Une première solution consiste à effectuer d'autres opérations sur les données durant ces cycles. En effet, le CRC est souvent inséré dans une chaîne de communication. Par exemple, dans le cas du DSL (Digital Subscriber Line)[2], le CRC est suivi d'une étape de brouillage (scrambling) qui peut être effectuée pendant les cycles d’attente. Cette solution n'est pas optimale mais elle est probablement utilisée dans certains dispositifs. On pourrait faire mieux en adaptant l'algorithme de division proprement dit. La solution présentée dans la suite se base sur une propriété des polynômes binaires: le reste de la division d'une somme de polynômes est égal à la somme des restes des divisions des polynômes (la preuve peut être trouvée dans [8]). Cette propriété est déjà utilisée, par exemple pour sommer des CRC partiels dans des cellules ATM (Asynchronous Transfert Mode), ainsi que pour pouvoir modifier le contenu de la cellule et le refléter dans le CRC, en ne calculant que le CRC de la modification. En effet, en cas de recalcul complet, les erreurs déjà présentes seraient intégrées dans le nouveau CRC [8]. Soit, On a M(x) un polynôme à coefficients binaires, Mi(x) tels que Mi(x) = M(x), R(x) et Ri(x) les restes des divisions respectives de ces polynômes par un polynôme générateur G(x). Ri(x) = R(x). Le principe adopté est de contourner la dépendance qui apparaît dans une division polynomiale binaire en exécutant de front plusieurs divisions, permettant ainsi un traitement parallèle des mots. Les données vont ainsi être réparties dans plusieurs polynômes (soit p le nombre de polynômes traités en parallèle). Une importance particulière doit être accordée à la manière dont ce principe est mis en œuvre car il ne faut pas alourdir la méthode classique au point de perdre tout bénéfice. Il est aussi primordial de conserver la lecture séquentielle des octets de données. On répartit donc successivement les octets dans chaque polynôme (voir Fig. 3). Les positions libres sont bien sur remplies de zéros. 1 2 3 1 4 6 7 4 2 6 3 8 ... 8 ... ... 7 ... Fig. 3 – Exemple de répartition des octets (p=3) [1] La table parallèle se calcule de la même façon que la table classique, en calculant le reste des données traitées lors de l'itération, c'est à dire 1 octet de données mais suivi cette fois de p-1 octets de zéros. Donc, lors du traitement d'un octet de données, la table fournit le résultat à combiner directement avec l'octet de données suivant, p positions plus loin (Fig. 4). DATA ZEROS DATA XOR TABLE Fig. 4 – Une étape au sein d’un polynôme On arrive ainsi à l'objectif fixé: les octets de données sont utilisés séquentiellement et une seule table est nécessaire pour toutes les divisions parallèles. En effet, la réorganisation des données n'est que virtuelle et il suffit d'accéder aux données en utilisant une indexation correcte. De même, les octets de zéros sont virtuels car ils sont pris en compte lors de la génération de la table parallèle. Ce faisant, une nouvelle étape est nécessaire pour rencontrer un double objectif: la combinaison des p restes partiels et le traitement des octets de données résiduels, si leur nombre n'est pas multiple de p. Les restes partiels apparaissent sur p positions successives et sont suivis d'au maximum p-1 octets de données. Les restes et les données peuvent être combinés et divisés comme un polynôme ; ce qui peut être encore vu comme une application du théorème précédent. Cette division peut se faire de la façon classique (avec une table), en utilisant la division binaire ou l'algorithme 'shift & add' [9]. Cette étape n'introduit que peu de calculs supplémentaires, d'autant plus négligeables que le nombre d'octets de données est grand. CRC1 CRC2 CRC3 CRC1 CRC2 CRC3 DATA DATA DATA DATA RESULT Fig. 5 – Combinaison des résultats partiels [1] Cette méthode a été mise en œuvre à l’aide du « DSP Starter Kit » de TI, équipé d’un DSP C6211. Les performances sont mesurées directement sur le hardware. Dans le cas présent, p=6 utilise au mieux les capacités du processeur, et permet de produire un résultat à chaque cycle. Le noyau de la boucle pipelinée exécute à chaque cycle deux instructions de chargement en mémoire (LOAD) et une soustraction (XOR). Les unités d'accès en mémoire sont alors complètement utilisées. L’algorithme a d’abord été programmé en C, le compilateur se chargeant de paralléliser le code. Nous avons cependant remarqué que le compilateur utilise 7 cycles pour le noyau de la boucle (problème de gestion des registres). Comme le code n’est pas trop compliqué, il a été écrit manuellement en langage assembleur. Le noyau de la boucle est présenté dans le tableau 1. Chaque colonne concerne une division, et la septième, le contrôle de la boucle. Une ligne représente les instructions traitées en parallèle. ) & # &, &, - # .,# # &, &, - .,# &, &, - # + .,# .,# &, &, - .,# # + &,, / $ 01/ &, &, * # * .,# &, - # + .,# $ %&'( * &, &, - Tableau 1 – Noyau de la boucle pipelinée [1] La méthode ci-dessus peut-être généralisée aux autres cas ( 2n-k). En prenant 3n-k, on peut accélérer l'exécution du CRC (8 bits) au prix d'une table beaucoup plus grande. Mais dans le cadre de la mise en œuvre logicielle, la lecture en mémoire de mots de bits n'est pas possible pour toutes les valeurs de 4Dans le cas présent, on ne peut atteindre 5 +, ce qui aurait permis de doubler la vitesse de traitement, car une table de 128 kilo-octets est prohibitive et dépasse la taille de la mémoire interne du processeur; la mémoire externe est quant à elle trop lente. CRC1 CRC2 Fig. 6 – Report du CRC partiel [1] Le second cas, <n-k, est beaucoup plus intéressant. Il englobe les CRC 16 et 32 bits en traitant 8 bits en parallèle. La généralisation n'est cependant pas triviale : le CRC s’étend maintenant sur plus d’un octet, alors que l’on continue à traiter 8 bits à la fois. Une partie du CRC déborde sur l’espace où il doit y avoir des zéros, mais il est cependant possible de contourner assez facilement cette difficulté. A chaque étape, uniquement le premier octet du CRC, qui se trouve à la bonne position, est gardé dans le polynôme. L'autre partie sera soustraite à celui-ci et reportée sur le suivant, puisqu’elle se trouve à la position de l’octet de donnée. Comme il s'agit de soustraire à un polynôme et d'ajouter à un autre, le même théorème s'applique encore. Dans le cas du CRC 16 bits, l'implémentation est directe (Fig. 6). On lit un octet, et via la table, on déduit la valeur partielle à combiner avec l’octet suivant, p-1 positions plus loin. Cependant, le résultat partiel s’étend sur 2 octets; la seconde partie sera donc répercutée sur l’octet de donnée du polynôme suivant. Classique crc = ((crc<<8) ^ data[i]) ^ table[crc>>8]; Parallèle (p=3) tmp1 = tablepar[(crc3&255) ^ (crc1>>8)] ^ data[i]; tmp2 = tablepar[(crc1&255) ^ (crc2>>8)] ^ data[i+1]; tmp3 = tablepar[(crc2&255) ^ (crc3>>8)] ^ data[i+2]; crc3 = tmp3; crc1 = tmp1; crc2 = tmp2; Tableau 2 – Exemple de noyaux C du CRC 16 bits Une itération de l'algorithme classique prenant 9 cycles (le cas général demandant plus d'opérations que le cas développé précédemment), on voudrait exécuter 9 divisions en parallèle. Ce n'est pas possible à cause de la 'pression' sur les registres, il y a en effet trop de valeurs temporaires à stocker; on arrive en pratique à p=8. Mettre en œuvre le même principe avec le CRC 32 demande plus de précautions: répartir directement les octets sur les autres polynômes demande trop d’opérations. Une solution est proposée; elle ne demande que peu d’instructions supplémentaires, mais elle introduit un cycle de latence supplémentaire. Cette fois, on utilise une valeur temporaire commune pour ce CRC, ce qui permet d’intégrer la valeur partielle fournie par la table en une seule opération. La dépendance causée par cette opération, oblige à traiter 1 octet par 2 cycles. En pratique, on travaille avec p=6, pour contrer la latence de l'accès en mémoire. Classique crc = ((crc<<8) ^ data[i]) ^ table[crc>>24]; Parallèle (p=3) tmp1 = tablepar[crc>>24] ^ data[i]; crc = (crc<<8)^tmp2; tmp2 = tablepar[crc>>24] ^ data[i+1]; crc = (crc<<8)^tmp3; tmp3 = tablepar[crc>>24] ^ data[i+2]; crc = (crc<<8)^tmp1; Tableau 3 – Exemple de noyau C du CRC 32 bits Les résultats relatifs aux différents algorithmes sont présentés ici. On montre d’abord les performances du CRC sur un processeur de signal classique (non-parallèle), afin d'avoir un point de référence. Il s'agit de résultats fournis par Texas Instruments pour un DSP TMS320C54xx; la mise en œuvre a été faite en langage assembleur [10]. Ensuite, on trouve les résultats de l'algorithme classique et de l'algorithme proposé. Ces derniers résultats ont été mesurés sur un DSP TMS320C6211. Le code assembleur à été généré à partir du code C, sauf dans le cas du CRC 8 où l'analyse du code assembleur révélait un problème évident d'optimisation. 10000 8000 6000 4000 2000 0 1 2 3 Fig. 7 – Nombre de cycles requis pour traiter 1024 octets [1] Dans la figure 7, les résultats de l'implémentation de l'algorithme classique par TI sur un TMS320C54x sont présentés en (1), en (2) les résultats de l'algorithme classique sur un TMS320C6211 et en (3) les résultats de l'algorithme proposé sur un TMS320C6211. Le CRC 8 bits est indiqué en blanc, le CRC 16 bits en noir et le CRC 32 bits en gris. Dans tous les cas, 8 bits sont traités en parallèle ( =8). Les architectures VLIW commencent à se répandre dans les processeurs, mais celles-ci dépendent fortement des performances du compilateur. Il existe cependant des algorithmes qui s’y adaptent mal, dont le très utilisé CRC. En effet, s'il y a un délai pour le chargement des valeurs en mémoire, aucun parallélisme ne peut être exploité. Dans un tel cas, la méthode proposée permet d'éviter la dépendance entre chaque itération et d'utiliser efficacement le software pipelining. Sur l’architecture étudiée, on parvient ainsi à traiter près d'un octet par cycle pour les CRC 8 et 16 bits et un octet par 2 cycles pour le CRC 32 bits. 7. Références # $ %&'( 6 1. -4 : & ; < =4-4 : "Word-Parallel CRC Computation on a VLIW DSP", Electronics Letters, IEE., vol.38, janvier 2002, p.64-65. 7 891/ 7 8"Standart project for Interaces Relating to Carrier to Customer Connection of Assymetrical Digital Subscriber Line", T1.413 Issue 2, ANSI, 1997. 78 # > 7 80< 4 : "Byte-wise CRC Calculation", IEEE Micro, 1983, p.40-50. #( < -4 : "Computation of cyclic redundancy checks via table lookup", Communications of the ACM, vol. 31, 1988, p. 1008-1013. 7*8#,00 ( 4 : "A painless guide to CRC error Detection Algorithms", $ ? @@ 1993. 4 4 @ @ 4 $ , 7+8"TMS320C6000 Technical Brief", spru197d, 1999, Texas Instruments. 768", < A: $ +: < B ' 4 4 : 9#'0< D0 D E4 : ,&; 04 : AEfficient computation of packet CRC from partial CRCs with application to the Cells-In-Frame protocol", Computer Communications, vol. 21, 1998, p. 654-661. 7C8#,;'D0E) 7F8 G &-H ' # -4 : AFast Software Implementation of Error Detection Codes", IEEE Transaction on networking , 1995, p. 640-651. # H' 4 : "Cyclic Redundancy Check Computation, An Implementation Using the TMS320C54x", Application Report spra530, 1999, Texas Instruments. 7 8;