Skip to content

Niveau 6 · Chapitre 6.9

Cœurs réels : x86, ARM et AVR comparés

Trois processeurs d’exemple classiques (le Core i7 Sandy Bridge d’Intel, le Cortex-A9 de l’OMAP4430 de TI et l’ATmega168 8 bits), ce qui a changé depuis 2011 chez Intel, AMD et Apple, et ce que fait réellement un cœur d’Apple M2 Ultra, mesuré : instructions par cycle, latence de chargement à chaque niveau de cache, et ses cœurs à haute efficacité.

Les chapitres précédents ont pris les idées une par une : pipeline, caches, prédiction de branchement, exécution dans le désordre. Un vrai cœur les utilise toutes à la fois. Ce chapitre regarde des conceptions réelles : trois exemples classiques des alentours de 2011, ce qui a changé depuis, et un cœur actuel mesuré directement.

Ces trois exemples couvrent toute la gamme. Le Core i7 est une puce x86 haut de gamme pour ordinateur de bureau. L’OMAP4430 est une puce de téléphone construite autour de deux cœurs ARM Cortex-A9. L’ATmega168 est un microcontrôleur 8 bits qui coûte environ un dollar. Tous trois étaient d’actualité vers 2011. Les puces ont beaucoup changé depuis ; les chiffres ci-dessous précisent donc la génération dont ils parlent.

Le Core i7 : un x86 sur un cœur de type RISC

Le Core i7 de 2011 repose sur la microarchitecture Sandy Bridge d’Intel. Vu de l’extérieur, il exécute le vieux jeu d’instructions x86 : instructions de longueur variable, peu de registres, adressage complexe. À l’intérieur, il ressemble à n’importe quel cœur RISC rapide. L’astuce est dans le front end :

  • Les décodeurs traduisent les instructions x86 en micro-opérations simples (µops), jusqu’à quatre par cycle.
  • Un cache de µops d’environ 1 500 entrées garde les µops décodées, pour qu’une boucle chaude évite le décodage. Intel expliquait que le but était autant l’énergie que la vitesse : quand le cache de µops fait mouche, les décodeurs peuvent dormir.
  • Le prédicteur de branchement oriente la lecture des instructions ; ses détails sont un secret industriel.
  • Le moteur d’exécution dans le désordre renomme les registres et inscrit chaque µop dans un tampon de réordonnancement (reorder buffer, ROB) de 168 entrées. Des ordonnanceurs envoient les µops prêtes vers six ports : trois avec des ALU entières (qui portent aussi les unités flottantes et de branchement), un pour les écritures et deux pour les lectures en mémoire.
  • Une unité de retrait valide les résultats dans l’ordre du programme, ce qui garde les exceptions précises.

Les caches : un L1 d’instructions et un L1 de données de 32 Ko (associatifs à 8 voies, lignes de 64 octets), un L2 privé de 256 Ko par cœur, et un L3 partagé allant jusqu’à 20 Mo.

Quelques faits sur cette puce méritent d’être énoncés précisément :

  • Huit registres généraux visibles, c’est la vue 32 bits. En mode 64 bits, le mode d’utilisation normal, x86-64 en a 16.
  • Les instructions x86 font de 1 à 15 octets. Quinze est la limite architecturale ; une instruction plus longue déclenche une exception.
  • AVX, introduit avec Sandy Bridge, a des registres de 256 bits, deux fois la largeur des registres SSE de 128 bits qui l’ont précédé ; c’est tout l’intérêt d’AVX.
  • La fréquence d’horloge a à peine augmenté après cette génération. Les Sandy Bridge de bureau montaient à environ 3,9 GHz en mode turbo ; les puces de bureau les plus rapides atteignent aujourd’hui environ 6 GHz, et la plupart des cœurs tournent bien en dessous. Les progrès sont venus depuis de cœurs plus larges, de caches plus grands et de cœurs plus nombreux, le sujet du reste de ce niveau.

Le Cortex-A9 : du RISC sans traduction

Le Cortex-A9 est un cœur ARMv7 32 bits, conçu par ARM et vendu sous licence à des fabricants comme Texas Instruments, qui a construit l’OMAP4430 autour de deux d’entre eux. Comme les instructions ARM sont déjà des opérations simples de registre à registre, il n’y a pas de couche de traduction : les instructions passent directement du décodage au renommage puis à l’émission, et s’exécutent dans le désordre dans des ALU entières, un multiplieur, une unité flottante et SIMD NEON optionnelle, et une unité de chargement-rangement.

Il a un pipeline profond avec prédiction dynamique des branchements, des caches L1 de 32 Ko à lignes de 32 octets et un L2 partagé de 1 Mo. Il a aussi un petit tampon de boucle rapide (fast loop) : une boucle serrée s’y exécute pendant que le cache d’instructions et le prédicteur se mettent en veille. Sur un téléphone, économiser l’énergie guide la conception autant que la vitesse.

L’ATmega168 : l’extrémité minimale

L’ATmega168 est un autre monde. Il a 32 registres de 8 bits, 16 Ko de flash pour le programme, 1 Ko de SRAM pour les données, 512 octets d’EEPROM, des temporisateurs et des ports d’E/S, le tout sur une seule puce, et tourne jusqu’à 20 MHz. Pas de cache, pas de prédicteur de branchement, pas de logique d’exécution dans le désordre. Son pipeline a deux étages : pendant qu’une instruction s’exécute, la suivante est lue. La plupart des instructions prennent un cycle ; les chargements, les branchements pris et les multiplications en prennent deux.

Une ALU de 8 bits signifie que l’arithmétique sur 32 bits se fait par morceaux. Voici a + b sur des entiers de 32 bits, compilé par le back end AVR de LLVM pour l’ATmega168 :

add32:
    add r22, r18
    adc r23, r19
    adc r24, r20
    adc r25, r21
    ret

Quatre instructions, une par octet, la retenue étant transmise par adc : l’additionneur à propagation de retenue du niveau logique numérique, fait en logiciel. Un cœur x86 ou ARM64 le fait en une instruction.

Cette simplicité est un atout. La durée de chaque instruction est connue exactement : un programmeur peut compter les cycles pour produire un signal précis sur une broche. Un cœur de bureau, avec ses caches et sa spéculation, ne peut rien promettre de tel.

Deux détails sont faciles à mal énoncer :

  • La famille AVR peut adresser jusqu’à 16 Mo de mémoire de programme grâce à des registres de page nommés RAMPX, RAMPY et RAMPZ. Ils n’existent que sur les plus gros membres de la famille : RAMPZ sur les AVR à plus de 64 Ko de flash, les autres sur la gamme XMEGA. L’ATmega168, avec ses 16 Ko de flash, n’en a aucun.
  • Tout petit qu’il est, l’ATmega168 a bien un pipeline : le recouvrement à deux étages lecture/exécution décrit plus haut explique pourquoi la plupart des instructions se terminent en un cycle.

Partout la même forme

Côte à côte, le Core i7 et le Cortex-A9 montrent une chose toujours valable : sous des jeux d’instructions très différents, ils ont le même cœur d’exécution. Tous deux exécutent des opérations simples à deux registres sources et un registre destination, une par cycle et par unité, dans un pipeline profond avec prédiction de branchement et caches d’instructions et de données séparés. La différence est la façon d’y arriver. Le front end x86 doit découper des instructions complexes en telles opérations ; les instructions ARM en sont déjà.

C’est exactement la progression de la Mic-1 à la Mic-4 : un jeu d’instructions qui n’a rien de RISC, décodé en micro-opérations placées dans une file. Le Core i7 ajoute l’exécution dans le désordre par-dessus. L’ATmega168, dans l’ordre et minuscule, est le plus proche de la Mic-1.

Quinze ans plus tard : plus large et plus profond

Depuis 2011, l’organisation est restée la même, et tout ce qu’elle contient a grandi. Voici les chiffres clés publiés par les fabricants pour leurs gros cœurs :

Sandy Bridge (2011)Golden Cove (2021)Lion Cove (2024)Zen 4 (2022)Zen 5 (2024)
FabricantIntelIntelIntelAMDAMD
Instructions x86 décodées par cycle46842 × 4
Entrées du ROB168512576320448
ALU entières35646

Golden Cove est le cœur performance des Core de 12e génération d’Intel, et Lion Cove celui de la série Core Ultra 200. Zen 4 et Zen 5 équipent les Ryzen 7000 et 9000 d’AMD et les EPYC serveurs correspondants. Le décodeur de Zen 5 est divisé en deux groupes de quatre.

Apple ne publie presque aucun de ces chiffres pour ses propres cœurs. Des analyses indépendantes des cœurs performance du M1, en 2020, ont trouvé un décodeur large de 8 instructions et une fenêtre d’instructions de plus de 600 entrées, plus grande que celle de n’importe quel cœur x86 de l’époque. Les Cortex-X actuels d’ARM pour téléphones sont de largeur comparable. Les descendants du Cortex-A9 n’ont pas rapetissé : les cœurs de téléphone rivalisent aujourd’hui avec ceux des portables.

Pourquoi plus large plutôt que plus rapide ? La fréquence est limitée par la puissance : la puissance dynamique croît avec la fréquence et avec le carré de la tension, et une fréquence plus haute exige une tension plus haute. Un cœur plus large en fait plus par cycle à la place. Mais le chapitre sur l’exécution dans le désordre a montré la limite : la largeur n’aide que si le programme contient du travail indépendant.

Mesurer un vrai cœur

La machine sur laquelle ce chapitre a été écrit est un Apple M2 Ultra. macOS décrit son organisation via sysctl :

$ sysctl hw.nperflevels hw.perflevel0 hw.perflevel1 hw.cachelinesize
hw.nperflevels: 2
hw.perflevel0.physicalcpu: 16
hw.perflevel0.l1icachesize: 196608
hw.perflevel0.l1dcachesize: 131072
hw.perflevel0.l2cachesize: 16777216
hw.perflevel0.cpusperl2: 4
hw.perflevel0.name: Performance
hw.perflevel1.physicalcpu: 8
hw.perflevel1.l1icachesize: 131072
hw.perflevel1.l1dcachesize: 65536
hw.perflevel1.l2cachesize: 4194304
hw.perflevel1.cpusperl2: 4
hw.perflevel1.name: Efficiency
hw.cachelinesize: 128

(Quelques lignes ont été retirées.) Il y a deux sortes de cœurs. Les 16 cœurs performance ont chacun un cache d’instructions de 192 Ko et un cache de données de 128 Ko (quatre fois le L1 de données du Sandy Bridge) et partagent un L2 de 16 Mo par groupe (cluster) de quatre. Les 8 cœurs efficacité sont plus petits : L1 de 128 Ko et 64 Ko, et un L2 de 4 Mo par groupe de quatre. Les lignes de cache font 128 octets, le double de la taille x86.

Quelle largeur ?

macOS n’indique pas la fréquence d’horloge, mais une chaîne d’additions dépendantes la mesure. Chaque add x1, x1, #1 a besoin du résultat précédent, et une addition prend un cycle : la chaîne avance donc à exactement une addition par cycle. Chronométrer 96 de ces additions dans une boucle, 20 millions de fois :

// 96 dependent adds per iteration (the benchmark generates the full list)
__asm__ volatile(
    "1:\n"
    "add x1, x1, #1\n"
    "add x1, x1, #1\n"
    /* ... */
    "subs %0, %0, #1\n"
    "b.ne 1b\n"
    : "+r"(n) : : "x1", "cc");

a donné de 0,305 à 0,31 ns par addition sur plusieurs exécutions : le cœur P tournait donc à environ 3,3 GHz. Puis le même nombre d’additions, réparties sur k registres indépendants :

Chaînes indépendantesAdditions par cycle
11,00
21,97
43,29
84,8
165,9
(des nop au lieu d’additions)7,8

Avec assez de travail indépendant, un seul cœur termine près de six additions par cycle, ce qui suggère six ALU entières, et près de huit instructions par cycle quand elles n’ont besoin d’aucune ALU, ce qui indique un front end large de 8. Les résultats ont été identiques d’une exécution à l’autre. C’est exactement l’effet de l’exécution dans le désordre dans le simulateur :

Out of order · Quatre chaînes indépendantes sur un cœur de largeur 4

À essayer : Passez de l’exécution dans l’ordre au désordre, changez la largeur, la fenêtre, la latence des chargements (4 = L1, 12 = L2, 40 = L3) et le renommage ci-dessous, et comparez les cycles, ou appuyez sur Edit pour essayer votre propre code.

renaming

Loading emulator…

Les douze additions prennent 5 cycles en largeur 4, 8 en largeur 2 et 14 en largeur 1. Appuyez sur Edit et remplacez-les toutes par add eax, 1 : cela prend alors 14 cycles quelle que soit la largeur. La largeur ne sert à rien sans instructions indépendantes.

Les cœurs efficacité, atteints en lançant le même programme avec taskpolicy -b (priorité d’arrière-plan), tournaient à environ 1,6 à 1,9 GHz dans ce mode et plafonnaient vers 4 additions et 5 nop par cycle. Les chiffres étaient plus bruités, car la fréquence changeait sans cesse, mais la conception est clairement plus étroite. Le chapitre sur les multicœurs revient sur la raison d’avoir deux sortes de cœurs sur une puce.

À quelle distance est la mémoire ?

La seconde mesure est la latence de chargement : une chaîne de pointeurs, chacun rangé dans une ligne de 128 octets différente, parcourue dans un ordre aléatoire pour que ni le préchargeur ni aucun prédicteur d’adresse ou de valeur ne puisse deviner la suivante. Chaque chargement a besoin du résultat du précédent : le temps par pas est donc la latence de l’endroit où se trouve la donnée.

Données parcouruesTemps par chargement≈ Cycles à 3,3 GHzOù est la donnée
16–128 Kio0,92 ns3cache L1 de données
256 Kio – 4 Mio5,5–6,6 ns18–22L2
8–48 Mio7–84 ns, variable-L2, cache système, défauts de TLB
128 Mio – 1 Gio128–135 ns~430DRAM

Les marches correspondent aux tailles données par sysctl : la latence saute juste après 128 Kio, la taille du L1. Un succès dans le L1 prend trois cycles ; la DRAM en prend plus de 400. Entre les deux, plusieurs effets se mêlent. Le L2 de 16 Mo est partagé par quatre cœurs, la puce a devant la DRAM un grand cache de niveau système dont Apple ne publie pas la taille, et avec des pages de 16 Kio, un parcours aléatoire sur plusieurs mégaoctets rate aussi dans le TLB. Ces lignes variaient d’un facteur deux d’une exécution à l’autre ; le tableau donne donc la fourchette.

Le chapitre sur les caches donnait 4–5 cycles pour un L1 typique et 60–100 ns pour la DRAM. Le L1 du M2 est plus rapide que la moyenne, et sa DRAM, vue par un seul cœur qui suit des pointeurs, plus lente : c’est le prix d’un grand système mémoire partagé, conçu pour le débit, que le chapitre sur les multiprocesseurs mesure.

Un cœur 8 bits à côté d’un cœur 64 bits

Mettons les deux extrêmes côte à côte. L’ATmega168 à 20 MHz termine au plus 20 millions d’instructions simples par seconde, sur 8 bits. Un cœur performance du M2, à 3,3 GHz et jusqu’à six additions par cycle, fait environ 20 milliards d’additions 64 bits par seconde : mille fois plus d’opérations, chacune sur huit fois plus de bits, et la puce a 24 cœurs. Le microcontrôleur se vend pourtant toujours par milliards, parce que pour un thermostat, une clé de voiture ou une commande de moteur, un nombre de cycles fixe et prévisible et quelques milliwatts comptent plus que la vitesse.

À retenir

  • Le Core i7 (Sandy Bridge) décode le x86 en µops, les met en cache et les exécute dans le désordre sur un cœur de type RISC avec un ROB de 168 entrées. Le Cortex-A9 fait de même sans traduction ; l’ATmega168 est un cœur 8 bits dans l’ordre, à deux étages, sans cache.
  • x86-64 a 16 registres généraux et des instructions de 15 octets au plus, les registres AVX font 256 bits, les fréquences ont à peine monté depuis 2011, et l’ATmega168 a un pipeline à deux étages mais pas de registres RAMP.
  • Depuis 2011, les cœurs sont devenus plus larges (de 4 à 6–8 instructions décodées par cycle) et plus profonds (de 168 à 320–576 entrées de ROB), sans beaucoup gagner en fréquence.
  • Mesuré sur un cœur performance de M2 Ultra : environ 3,3 GHz, jusqu’à ~6 additions et ~8 instructions par cycle avec du travail indépendant, 1 addition par cycle sans, et des latences de chargement de 3 cycles (L1), ~20 cycles (L2) et ~430 cycles (DRAM).
  • La même puce mélange gros et petits cœurs, et les plus petits cœurs du monde restent des microcontrôleurs 8 bits.

Dans ce niveau

  1. 6.1Le cycle fetch–decode–execute
  2. 6.2Chemin de données et bus
  3. 6.3Unité de contrôle et microcode
  4. 6.4Une machine complète : la Mic-1 exécutant IJVM
  5. 6.5Pipeline et aléas
  6. 6.6Caches et hiérarchie mémoire
  7. 6.7Prédiction de branchement
  8. 6.8Exécution dans le désordre, renommage de registres et spéculation
  9. 6.9Cœurs réels : x86, ARM et AVR comparés
  10. 6.10SIMD, GPU et coprocesseurs
  11. 6.11Multicœurs, multithreading et cohérence de cache
  12. 6.12Multiprocesseurs à mémoire partagée et NUMA
  13. 6.13Grappes, passage de messages et supercalculateurs