• Et l'architecture alors ?

Revenons aux bases : un processeur est fait de transistors. Sans rentrer dans les détails de la physique des tripôles, un transistor permet de créer un "interrupteur" : le courant passe (ou non) suivant la valeur logique d'un troisième fil. A partir de cette brique de base sont fabriquées des bascules, un assemblage de transistors pouvant retenir une valeur (on a alors un registre à 1 bit !) ainsi que des portes logiques, c'est à dire des unités permettant la résolution matérielle de problèmes d'algèbre booléenes. Si le mot algèbre vous a déclenché des remontées acides, pas de problèmes : il s'agit simplement de calculs ou d'opérations logiques ("ou", "et", ...) apliqués à des nombres binaires.

 

Petite parenthèse : ce mode de codage n'autorise que les entiers à être représentés. Pour des nombres à virgule (ou des nombres très grands), il faut trouver une autre solution : c'est la représentation à virgule flottante, qui se base sur la multiplication ou la division d'un entier relatif par une puissance de 2. Nous ne couvrirons pas les détails ici, il faut simplement retenir que les unités du calcul nécessaires aux opérations flottantes sont bien distinctes de celles opérant sur les entiers (et plus lentes, en particulier pour la division). Notamment, on parle d'ALU (Unité Arithmétique et Logique) pour les calcul sur des entiers, et FPU (Unité à Virgule Flottante) pour les décimaux.

 

Les anciens processeurs nécessitaient un coprocesseur distinct pour effectuer des calculs de nombre à virgule : il fallait l'acheter séparément ! Ce dernier a été incorporé dans le CPU depuis l'Intel 486DX, lancé en 1989.

 

L'Intel 486 DX [cliquer pour agrandir]

L'i486-DX : autant au niveau du dessus que du dessous, cela a bien changé ! 

 

Point historique : la course à la fréquence

Autrefois, les processeurs étaient constitués d'un assemblage de transistors qui se contentait de lire une à une les instructions assembleur et de les exécuter les unes à la suite des autres (parle d'exécution séquentielle). La fréquence était alors le facteur déterminant la performance du CPU. Exprimée en Hertz (Hz), elle donne le nombre d'instructions effectuées par seconde. Ainsi, qu'un CPU soit Intel ou AMD, un processeur cadencé à 200 MHz était deux fois plus rapide qu'un autre cadencé à 100Mhz. Pour fournir des processeurs de plus en plus véloces, la fréquence a dûe être augmentée, atteingnant des sommets avec le Pentium 4 chez Intel, où un problème de taille s'est révélé : le dégagement thermique.

 

Alors que les bleus annonçait des processeurs à 10 GHz comme limite, l'architecture du Pentium 4 s'est arrété un peu de dessous des 4 GHz, car il devenait impossible de dissiper l'énergie nécessaire à l'alimentation des machines. Ce phénomène provient d'une loi physique stipulant que le dégagement thermique suit la tension de manière quadratique et la fréquence linéairement : si vous doublez la fréquence, et vous doublez la chaleur, sans compter l'augmentation de tension nécessaire à stabiliser cette cadence plus élevée... Depuis, de nouveaux mécanismes ont été développés pour augmenter l'IPC (nombre d'Instruction Par Cycle). C'est pourquoi il est aujourd'hui difficile de comparer les performances relatives des processeurs entre eux, car certains mécanismes sont présents et d'autres non, et leur impact peut être très prononcé selon l'utilisation.

 

C'est de ce principe qu'est né l'overclocking. Un dossier entier serait nécessaire aux explications matérielles et logicielles, mais le principe est le suivant : l'augmentation de la fréquence de fonctionnement du CPU. Même aujourd'hui, un i5-6600k à 4 GHz est toujours deux fois plus puissant que tous les autres i5-6600k à 2 GHz. Par contre, aucune comparaison n'est possible théoriquement sans benchmark face à un i7-4790K à 4 GHz.

 

Gagner des performances apacher : le Boost

On ne va pas s'appesantir là-dessus : le principe est juste d'augmenter la fréquence en fonction de la charge de travail demandée et du nombre de cœurs solicités (pour la description complète d'un cœur, attendez la seconde partie !). Plus le programme nécessite de cœurs et moins la fréquence sera augmentée. Sous boost, le CPU fait donc plus d'opérations par seconde, accélérant ainsi l'exécution au prix d'une consommation supérieure. De nos jours, le phénomène inverse existe également : réduire la fréquence au repos pour diminuer la chauffe : par exemple un i5-6600k mouline à 3,5 GHz de base (lors d'un encodage vidéo très lourd utilisant les 4 cœurs par exemple), mais il grimpe à 3,9 GHz si un seul coeur est utilisé ; par contre - hors charge - sa fréquence dégringole à 800 MHz, pas de panique donc si cela vous arrive !

 

Le pipeline

Une amélioration apparue chronologiquement très tôt est le pipeline. Il permet à plusieurs instructions de s'exécuter en même temps, sans pour autant changer la manière de programmer. Le principe consiste à découper l'exécution d'une instruction en plusieurs étapes, de manière à pouvoir exécuter le début de l'instruction suivante alors que l'instruction courante n'est pas encore terminée. Exemple classique : dans un restaurant self-service, vous n'attendez pas que la personne devant vous ait fini de payer pour prendre votre plateau, mais au contraire vous avez juste une étape de retard par rapport à elle.

 

Une des raisons de l'échec du Pentium 4 fut l'utilisation d'un pipeline bien trop long, entre 20 et 31 étapes (on parle aussi de stages). Bien que cela ait permis une montée en fréquence plus aisée (plus il y a d'étapes, moins il y a de choses à faire en une étape, et plus la fréquence peut augmenter), cela entraîne des surcoûts important lors des instructions de branchment, car il faut alors vider le pipeline le temps que le compteur ordinal soit mis à jour vers la nouvelle adresse.

 

Exemple : un processeur RiSC-16 pipeliné à 5 étages

Le pipeline d'un CPU RiSC-16 [cliquer pour agrandir]

Un RiSC 16 bits et son merveilleux pipeline (Crédit : Université de Maryland)

 

IdentifieurNom completFonction
IF Instruction Fetch Chercher la prochaine instruction
ID Instruction Decode Séparer les différentes opérandes en fonction de l'opcode, aller chercher les valeurs conenues dans les registres
EX Execute Stage Effectuer les calculs
MEM Memory Lire/Ecrire en RAM
WB Writeback Ecrire - si besoin - dans le registre cible

 

On notera qu'à chaque cycle, les valeurs de l'opcode et du registre de destination sont transmises à l'étage suivant (carrés "OP" et "rT", à droite). Le Register File contient les valeurs stockées dans les registres, alors que l'Instruction Memory contient le programme exécuté (sur les systèmes actuels, il est fusionné avec la RAM), et Data Memory n'est autre que cette RAM.

 

Les prédicteurs de branchement

Le principe est relativement simple : pour éviter d'attendre lors d'un branchement conditionnel (va ici, sinon continue), le processeur va faire des suppositions sur les instructions à charger. Dans le cas ou l'addresse est correctement prédite, l'exécution continue ; mais le pipeline doit être vidé dans le cas contraire, entraînant un temps suplémentaire. Ce mécanisme est néanmoins particulièrement efficace dans le cas de boucles, ou les branchements sont très prévisibles et très impactants sur les performances.

 

Les premiers prédicteurs de branchement étaient assez sommaires, en se contentant de charger à la dernière adresse utilisée lors de la dernière exécution du branchement ; la plupart de ceux utilisés de nos jours sont implémentés avec un compteur à 4 états (oui sûr/oui/non/non sûr), que l'on modifie en fonction du branchement réellement pris. Cela permet, dans le cas des boucles, de ne se tromper qu'une à deux fois (à la sortie de la boucle et éventuellement à la première itération). En effet, la condition de sortie n'étant vraie qu'une seule fois, ce n'est pas suffisant pour faire changer la prédiction d'un système à 4 états (on passe d'un état "oui sûr" à "oui" en se trompant à la sortie de la boucle) : le prédicteur de branchement peut ainsi se révéler particulièrement efficace.

 

Il existe d'autres techniques tels l'Eager Execution consistant à évaluer les deux chemins d'exécutions possibles et ne garder par la suite que le bon. De plus, les Chargement Spéculatifs (Speculative Loads) permettent de charger en avance une variable avant de connaître son adresse exacte. Par exemple, lorsque l'on souhaite accéder à des pointeurs de pointeurs, du type :

maClasse->suivante->suivante->monAttribut

 

Le processeur est théoriquement obligé d'attendre la valeur de :

maClasse->suivante

 

Avant d'accéder encore au second champ :

suivante

 

Et enfin à la valeur :

monAttribut

 

...souhaitée. Avec des chargements spéculatifs (qui se révèlent efficaces surtout lors d'opérations sur des tableaux), le processeur peut "deviner" à l'avance la valeur contenue dans maClasse->suivante et ainsi accélérer l'exécution.

 

Out-of-Order et les micro-opérations : décomposer pour mieux régner

Une évolution notable a été le passage à un fonctionnement de type OoO : Out-of-Order. Pour comprendre ce que cela signifie, il est nécessaire de garder en tête que toutes les instructions ne prennent pas le même temps à s'exécuter. Par exemple, la chargement d'une variable depuis la RAM peut prendre jusqu'à 200 cycles d'horloge, là ou une addition de deux registre ne prends que quelques cycle ! Pour éviter de bloquer le CPU en attendant un chargement mémoire, les autres instructions indépendantes peuvent être exécutées. Il en résulte que l'ordre d'exécution des instructions n'est pas le même que leur ordre de soumission. De manière plus large encore, les instructions présentent dans une certaine fenêtre (224 instructions sur l'architecture Skylake) autour de l'instruction courante peuvent être totalement réordonnées en fonction de ce que le CPU juge optimal. Pas de panique, ce fonctionnement est totalement transparent du point de vu de programmateur, si bien que - hors failles de sécurité - aucun changement dans le code n'est nécessaire pour tirer parti de cette amélioration.

 

Pour ce qui est du vocabulaire, un CPU qui n'est pas Out-of-Order est dit In Order. C'était le cas des premiers Atom, d'où leur performances ; et de nombreux processeurs ARM (citons par exemple le Cortex-A53 présent dans le Snapdragon 435). Pour les Out-of-Order, ce sont tous les autres : la gamme d'AMD, la gamme desktop et serveur d'Intel, ainsi que les plus gros cœurs ARM (par exemple les cœurs Kryo des Snapdragons 825).

Nous avions vu auparavant que les processeurs pouvaient être RISC (peu d'instructions disponibles, mais assez rapides) ou CISC (instructions plus puissantes et plus lentes). L'x86 était à la base du CISC, mais a dû évoluer pour apporter toujours plus de performances. Les instructions complexes sont désormais découpées en micro-ops ou micro-instructions, plus facile à réordonner et exécuter. Une fois l'instruction coupée en morceaux, les micro-ops sont redirigées vers des ports reliés aux unités de traitement correspondantes.

 

Les instructions vectorielles

Une manière simple et relativement efficace de gagner en performance est de rajouter des nouvelles instructions, appelées aussi extentions, car elle étendent celles de l'ISA de base, par exemple en permettant d'additionner deux tableaux via des unitées de calcul dédiées. Depuis le premiers CPU x86, de nombreuses intructions ont été ajoutées, en commençant par le MMX (MultiMedia eXtention, utilisées pour le décodage vidéo) introduit en 1995, jusqu'au SSE puis SSE2/SSE3, et enfin l'AVX / AVX-2 / AVX-512 intégré dans Skylake-X. Si ces dernières sont dites vectorielles, c'est parce qu'elles permettent de traiter directement des vecteurs, c'est à dire des tableaux, des zones contigues en mémoire, sur lesquelles une même opération est itérée. On parle alors de SIMD, pour Single Instruction Multiple Data : une seule instruction mais plusieurs opérations.

 

cpu z 4790k

On peut voir les extentions supportées avec CPU-Z, case "Instructions" (MMX, SSE, AVX, DTC, ...). Toutes ne sont par contre pas vectorielles !

 

Cependant, les programmes doivent être recompilés et les compilateurs réécrits pour tirer parti de ces nouvelles instructions ; de plus, le gain n'est pas toujours en rendez-vous dans des cas pratiques. Il suffit en effet que l'instruction vectorielle adaptée à votre programme vous permette d'additionner deux tableaux de taille 200 et que vous n'avez que 100 valeurs : on se doute bien que le CPU sera sous-utilisé.

 

Focus sur le SSE et l'AVX

Pour comprendre un peu plus en détails la vectorisation, penchons-nous sur les instructions SSE et leurs améliorations, l'AVX. A la base, les extentions Streaming SIMD Extentions (SSE !) proposaient 8 registres 128 bits nommés xmm0 jusqu'à xmm7, ainsi que les opérations de base sur ces registres : chargement, rangement, conversion (chargement depuis un registre général), et les classiques opérations arithmétiques telles que l'addition/soustraction/multiplication/division/inverse/racine/inverse de la racine/MAX/MIN, toutes pouvant opérer soit sur une seule valeur (Scalar) stockée dans un registre xmm, soit sur 4 flottants 32 bits contenus toujours dans une registre xmm (Packed). Le SSE2 vient élargir ces fonctionnalité en ajoutant des calculs sur les entiers et la double précision : on peut alors faire tenir (et effectuer les opérations sur) 2 entiers/flottants 64 bits, ou bien 4 en 32 bit, ou encore 8 entiers 16 bits, et même 16 valeurs 8-bits. Le SSE3 ajoute par la suite des instructions permettant la prise en charge d'une même opération sur deux couples de quatre registres.

 

D'autres instructions tels que les permutations des valeurs au sein d'un registre xmm et le produit scalaire sont rajoutées par la suite dans les extensions SSSE3 et SSE4. Pour replacer dans la chronologie Intel, le SSE apparait avec les Pentium III en 1999 ; et s'améliorera jusqu'à l'AVX (Advance Video eXtensions), introduit sur Sandy Bridge en 2013. Ces nouvelles instructions étendent simplement les opérations flottantes du SSE en rajoutant 8 autres registres et en doublant leur taille pour atteindre 256 bits. Les registres sont alors renommés en ymm, mais pour des questions de compatibilités, les xmm restent toujours utilisables.

 

L'AVX2 comble assez logiquement les trous de l'AVX en supportant les opérations entières sur les ymm. Enfin, l'AVX-512, uniquement présent sur les Xéon Phis KNL et les Skylake-X, doublent encore une fois la taille et le nombre de registres pour passer à 32 registres 512 bits nommés zmm.

 

Si vous compilez un programme, il y a de fortes chances que vous n'utilisiez pas ces extentions, le paramètres par défaut de GCC étant de sortir du code uniquement SSE. Pour compiler spécialement pour votre machine, il faudra spécifier -march=native. De plus la vectorisation (SSE en mode Packed) est désactivée si vous n'utilisez pas -O3.

 



sommaire

1 • Préambule
2 • Registres, assembleur et instructions
3 • Dans les transistors
4 • Conclusion (avant la suite)

Les 14 Ragots
   
Les ragots sont actuellement
ouverts à tous, c'est open bar !