Adaptation du calcul du CRC à une architecture parallèle à

publicité
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;
Téléchargement