22

Lever le voile sur la table de cycles du Cortex A8 : Part 1

déc

A mon époque – puisque je date d’une autre époque – lire une table de cycle était relativement simple.
L’ARM3 disposait d’un pipeline à 3 niveaux:

  • Décodage
  • Execution
  • Ecriture

Du coup lire la table de cycle revenait à prendre le temps d’exécution de l’instruction et c’était à peu prêt tout.
Ce n’est plus du tout le cas à présent. Même si une fois que l’on a compris tout semble logique, il faut tout de même un petit peu d’effort pour bien assimiler le concept et beaucoup de travail pour calculer en amont le nombre de cycles que va prendre telle ou telle fonction.

Le cas général

Intéressons-nous à un cas particulier
Soit le code suivant:

	vshll.u16	q10, d27, #6						@ Y * 16384 (32b)
	vmlsl.s16	q10, d29, d1[1]					@ Y - U * 5638 (32b)
	vmlsl.s16	q10, d31, d1[2]					@ Y - U * 5638 - V * 11700 (32b)
	vshll.u16	q11, d27, #6						@ Y * 16384 (32b)

nous avons deux instructions dont les tables de cycles sont les suivantes:

inst.     Format       Cycle  Source1  Source1  Source3  Source4 Result1 Result2
--------------------------------------------------------------------------------
VSHL      Qd,Dm,[#IMM] 1      Dm:N1    -        -        -       QdLo:N3 QdHi:N3
VMLS      Qd,Dn,Dm[x]  1      Dn:N2    Dm:N1    QdLo:N3  QdHi:N3 QdLo:N6 QdHi:N6

Cette table nous indique à quel moment les instructions vont avoir besoin d’accéder aux registres (Source) et à quel moment ils vont fournir le résultat (Result). Elle nous indique aussi combien de cycle elle vont bloquer le pipeline NEON (c’est le nombre de cycle).

En fait les instructions ne vont pas s’exécuter en un cycle, par conte à chaque nouveau cycle il sera possible d’en lancer une nouvelle.

l’exécution du programme est le suivant:

			vshll.u16				vmlsl.s16 #1			vmlsl.s16 #2			vshll.s16
Cycle 0	Execution				-							-							-
Cycle 1	Lecture Dm(d27)		Execution				-							-
Cycle 2	Calcul					Lecture Dm(d1[1])		Execution				-
Cycle 3	Ecriture Qd(q10)		Lecture Dn(d29)		Lecture Dm(d1[2])		Execution
Cycle 4	-							Lecture Qd(q10)		Lecture Dn(d31)		Lecture Dm(d27)
Cycle 5	-							Calcul					Lock !!!					Calcul
Cycle 6	-							Calcul					Lock !!!					Ecriture Qd(q11)
Cycle 7	-							Ecriture Qd(q10)		Lecture Qd(q10)
Cycle 8	-							-							Calcul
Cycle 9	-							-							Calcul
Cycle 10	-							-							Ecriture Qd(q10)

Que remarque t-on dans cet exemple:

  • Nos instructions ont débutées aux cycles 0, 1, 2 et 3
  • Elle s’exécutent en parallèle. c’est à dire que la première n’est pas encore terminée que la deuxième est initiée
  • La 3eme instruction est obligé d’attendre 2 cycles (marqués Lock !!!) avant de pouvoir récupérer q10.
  • Le Lock de l’instruction 3 ne bloque pas l’exécution de la 4eme instruction qui n’a pas de dépendance avec les précédentes qui par ailleurs va se terminer avant la précédente.
  • Le calcul du nombre de cycle est donc très compliqué. Combien notre exemple prend t-il de cycles? Il lui en faut 10 pour se terminer complètement, mais dans la pratique, NEON est disponible pour exécuter une nouvelle instruction dès le 5eme cycle.

Comment savoir combien de temps une instruction ?
En fait on ne s’intéresse pas tant au temps que prend l’instruction pour s’exécuter qu’au cycle à laquelle elle commence. Ainsi, on peux simplement indiquer à quel cycle l’instruction commence et si avant cette instruction il y a eu des cycles où aucune autre instruction n’a pu commencer.

Le calcul du nombre de cycles est donc difficile à réaliser au niveau de l’instruction. Il doit se faire au niveau d’un ensemble d’instructions.
De plus, si la lecture des table de cycles s’applique en général, il existe de nombreux cas particuliers qui vont venir perturber votre calcul.

Les court-circuits

Le Cortex dispose de certaines caractéristiques qui permettent dans certains cas particuliers (documentés) de réduire les dépendances entre deux instructions particulières.

C’est le cas par exemple de nos deux instructions vmlsl.
En fait, la deuxième instructions ne va pas devoir attendre et les cycle de Lock sont en réalité inexistants.
Lorsque le Cortex trouve deux instructions de multiplications ayant le mémé accumulateur (ici q10) alors il exécute un court circuit qui consiste à:

  • Publier l’écriture du registre destination un cycle plus tôt. C’est à dire que q10 ne sera pas disponible au Cycle 7 mais au cycle 6
  • De plus la deuxième instruction est modifiée afin de ne plus avoir besoin de l’instruction au 3eme cycle de son exécution mais au 4eme.

Donc dans ce cas l’éxecution de notre programme est le suivant

			vshll.u16				vmlsl.s16 #1			vmlsl.s16 #2			vshll.s16
Cycle 0	Execution				-							-							-
Cycle 1	Lecture Dm(d27)		Execution				-							-
Cycle 2	Calcul					Lecture Dm(d1[1])		Execution				-
Cycle 3	Ecriture Qd(q10)		Lecture Dn(d29)		Lecture Dm(d1[2])		Execution
Cycle 4	-							Lecture Qd(q10)		Lecture Dn(d31)		Lecture Dm(d27)
Cycle 5	-							Calcul					Calcul					Calcul
Cycle 6	-							Transfert Qd(q10)		Lecture Qd(q10)		Ecriture Qd(q11)
Cycle 7	-							Ecriture Qd(q10)		Calcul					-
Cycle 8	-							-							Ecriture Qd(q10)		-
Cycle 9	-							-							-							-
Cycle 10	-							-							-							-

Le transfert anticipé (de l’instruction 1) + le besoin retardé de l’accumulateur (de l’instruction 2)
permet finalement à la deuxième instruction de ne pas prendre de retard sur son exécution.

Les délais additionnels

A contrario de l’optimisation précédente il y a des couples d’instructions qui peuvent engendrer un cycle d’attente supplémentaire.
c’est le cas par exemple d’une instruction vmax suivi par un décalage de bits.

Par exemple, le code suivant devrait s’exécuter sans aucun perte de cycle et pourtant il y a bien un cycle de pénalités lors de l’exécution de l’instruction vqrshrn.

Ce comportement est lié a la spécificité des deux instructions vmax et vqrshrn et de leur succession (dans cet ordre).
Le problème est que la liste de ces couples engendrant ce ou ces cycles de pénalités ne sont pas documentées. Il y a juste une note dans la documentation indiquant que cela peut arriver.

Délais due au Writeback

Une autre source de cycle de pénalité vient du WriteBack.
Il se peut (et ce n’est pas documenté non plus) qu’une instruction se terminant avant l’instruction qui la précède doive attendre que la première se termine.
Dans notre exemple initial, notre 4eme instruction se termine avant la 3eme.
Il se peut donc (mais ce n’est pas le cas dans l’exemple) que le cortex décide d’attendre que la multiplication (vmlsl) se termine avant d’autoriser une 5eme instruction à utiliser q11 (pourtant disponible 2 cycles plus tôt).

La mémoire

Enfin, la vitesse des accès à votre mémoire et le nombre d’accès que vous votre programme va réaliser pourront avoir un impact très significatif sur le temps “calculé” de votre code.

Bref…

Le calcul de cycles n’est pas aisée. Rien que la définition de ce que prend réellement une instruction n’est pas trivial.
Sans parler du fait que l’on a simplement survolé le fonctionnement du pipeline de NEON. Pour réellement aller au bout des choses, il faudrait se pencher sur les “unités fonctionnelles” de NEON qui permettent de mieux comprendre à quel moment une nouvelle instruction peut débuter et a quel moment NEON va devoir attendre.

Mais bon ça sera pour une prochaine fois.

 | Tags:

One Response to “Lever le voile sur la table de cycles du Cortex A8 : Part 1”

  1. [...] de mon précédent article sur le fonctionnement du pipeline de NEON, je n’avais pas voulu entrer trop dans les détails. Pourtant, pour avoir une bonne [...]

Répondre

Human control : 1 + 5 =