Aller au contenu

Architectures des jeux d'instructions

De nombreux facteurs influencent les performances d’un ordinateur, c’est-à-dire le temps de calcul que le programme utilise pour traiter les données. L’énergie nécessaire à ces traitements est également un facteur déterminant pour la mise en oeuvre d’un système embarqué ou d’un objet de l’Internet.

Performances et temps d’exécution

D’un point de vue logiciel, trois facteurs principaux sont à considérer pour évaluer les performances d’un processeur (Iron Law):

  • le nombre d’instructions contenues dans le programme
  • le nombre de cycles d’horloge nécessaires pour exécuter une instruction
  • le temps d’un cycle d’horloge
\[\frac{\mathsf{Time}}{\mathsf{Program}} = \frac{\mathsf{Instructions}}{\mathsf{Program}} \times \frac{\mathsf{ClockCycles}}{\mathsf{Instruction}} \times \frac{\mathsf{Time}}{\mathsf{ClockCycles}}\]

Le nombre d’instructions dans un programme est naturellement largement dû à la complexité du problème à solutionner et de l’application qui en découle. Le choix des structures de données et d’algorithmes appropriés est primordial à la réalisation d’un code performant. Si le temps d’un cycle d’horloge (CP - Clock Period) ne dépend que de la fréquence de l’horloge système, le nombre de cycles d’horloge pour exécuter une instruction élémentaire (CPI - Clock Per Instruction) dépend quant à lui principalement de l’architecture du jeu d’instructions (ISA - Instruction Set Architecture) du processeur et de son implémentation.

Le temps nécessaire à l’exécution d’un programme ou d’un algorithme se calcule en multipliant ces trois facteurs:

\[\mathsf{Time} = \mathsf{Instructions} \times \mathsf{CPI} \times \mathsf{CP}\]

Avec l’évolution des processeurs, différentes architectures du jeu d’instructions (ISA - Instruction Set Architecture) coexistent sur les différentes familles de processeurs. Actuellement, on distingue généralement deux architectures principales:

  • CISC : Complex Instruction Set Computer
  • RISC : Reduced Instruction Set Computer

Hormis les processeurs de la famille x86 d’Intel, y compris les versions 64 bits, qui utilisent un jeu d’instructions étendu (CISC), la plupart des autres processeurs modernes utilisent un jeu d’instructions réduit (RISC) et en particulier les processeurs ARM.

Architecture CISC

Les processeurs de l’architecture CISC disposent d’un très grand nombre d’instructions, certaines très complexes, mélangeant opérations arithmétiques ou logiques avec modes d’adressages complexes. La taille des instructions est de longueur variable. Le nombre de cycles nécessaire à l’exécution des instructions est variable et dépend de ces dernières.

L’exemple ci-dessous montre une simple incrémentation d’une variable en C.

    int a = 15;
    void inc() { a++; }

Après compilation de ce code pour un processeur x86 à 64 bits, on obtient le code machine suivant:

    100003f90 _inc:
    100003f90: 55                  pushq %rbp
    100003f91: 48 89 e5            movq %rsp, %rbp
    100003f94: ff 05 6a 00 00 00   incl 106(%rip)
    100003f9a: 5d                  popq %rbp
    100003f9b: c3                  retq

Deux caractéristiques principales des machines CISC sont facilement visibles, premièrement la taille variable des instructions et deuxièmement l’instruction incl permettant d’incrémenter directement de la variable a dans la mémoire sans devoir préalablement charger son contenu dans un registre interne du µP pour effectuer cette incrémentation.

Architecture RISC

Les processeurs de l’architecture RISC disposent d’un jeu d’instructions simple, facile à décoder et de taille fixe. Cette taille fixe facilite grandement l’architecture de la mémoire cache des instructions et la mise en oeuvre d’un pipeline. Grâce au pipeline, chaque instruction est exécutée en “un” cycle d’horloge (CPI = 1.0). Cette architecture permet également de réduire la perte d’énergie par dissipation thermique.

Pour des raisons de simplicité, les processeurs RISC ne disposent pas d’instructions permettant d’exécuter des opérations arithmétiques et logiques directement sur des données en mémoire. Par conséquent, le µP doit préalablement charger la donnée dans un de ses registres internes pour être traitée et finalement restockée dans la mémoire. On parle d’architecture Load-Store.

Après compilation du code C de l’exemple ci-dessus incrémentant une variable, on obtient le code machine suivant pour un processeur ARMv7, µP à 32 bits:

    80001120: e59f200c   ldr r2, [pc, #12]
    80001124: e5923000   ldr r3, [r2]
    80001128: e2833001   add r3, r3, #1, 0
    8000112c: e5823000   str r3, [r2]
    80001130: e12fff1e   bx lr
    80001134: 80006a00   .word 0x80006a00

Avec l’exemple ci-dessus, il est aisé de constater que la taille des instructions est bel et bien fixe.

Toutes les instructions étant de taille fixe, le µP ne peut obtenir l’adresse de la variable a que de manière indirecte. Pour cela, il charge dans un de ses registres internes un mot de 32 bits stocké dans le code en utilisant l’adresse courante du compteur ordinaire (PC - Program Counter) et une offset (un décalage). Ce mot de 32 bits est le résultat du compilateur et de l’éditeur de liens qui stockent l’adresse de la variable dans le code, ci-dessus après la dernière instruction de la routine. Le compilateur calcule l’offset correspondante au décalage entre l’adresse de l’instruction de chargement de l’adresse de la variable a à sa position dans le code. Dans l’exemple ci-dessus, le décalage est de 20 octets (0x80001134 - 0x80001120), mais l’offset n’est que de 12 octets, car le PC sur les µP ARMv7 est déjà incrémenté de 8 octets lors de l’exécution de l’instruction. On remarque également que pour incrémenter la variable a, le µP doit effectuer trois opérations, une première de lecture de la valeur de a (ldr r3, [r2]), puis l’incrément de la variable dans le registre du µP (add r3, r3, #1, 0) et finalement le stockage du résultat dans la mémoire (str r3, [r2]).