Enseignements en L3
Mention informatique

 


3. Microprocesseur

L'informatique n'est pas plus la science des ordinateurs que l'astronomie n'est celle des télescopes.
Edsger Dijkstra


3.1 Introduction

Pour comprendre comment fonctionne un ordinateur, il faut avant tout comprendre son mode de fonctionnement interne.

Les circuits électroniques sont capables de réaliser des calculs complexes en utilisant une représentation de l'information binaire.

Nous nous focaliserons plus particulièrement, au cours de ce chapitre, sur le microprocesseur vu en tant qu'élément d'un dispositif permettant d'exécuter les programmes. Notre but étant d'apprendre à programmer en assembleur, le langage des microprocesseurs.


3.1.1 L'invention du transistor
L'invention du transistor date du 23 Décembre 1947, lorsque William Shockley, Walter Brattain et John Bardeen mirent au point le transistor avec point de contact (point-contact transistor) aux Bell Laboratories.

Leur invention fut révélée au public en 1948 et ils obtinrent le prix Nobel en 1958.

"L'une des plus importantes découverte du 20ème siècle, le transistor, fut une conséquence de la recherche sur les radars effectuée aux USA et en Grande Bretagne durant la seconde guerre mondiale. Alors qu'ils travaillaient sur une nouvelle méthode de détection d'objets volants, les scientifiques ont commencé à étudier une gamme de solides peu connus appelés semiconducteurs. Des matériaux comme le Silicium et le germanium, qui occupent la même colonne dans la table périodique des éléments, semblaient disposer d'un grand potentiel en tant qu'amplificateurs de courant et apparaissaient comme des substituts intéressants des tubes à vides."
(voir [Augarten 83], page 2)

Le but du projet initié aux Bell Labs en 1945 était de trouver une alternative au tubes à vides qui souffraient de plusieurs défauts :
  • ils étaient fragiles,
  • leur production coûtait relativement cher,
  • ils consommaient énormément d'énergie que ce soit en tant qu'amplificateur ou qu'interrupteur.
L'ampoule électrique fut inventée par Henry Woodward en 1874. Son invention fut ensuite améliorée par Thomas Edison, puis John Ambrose Fleming qui créa la diode en 1904. Enfin Lee De Forest eut l'idée en 1906 d'ajouter troisème electrode à la diode et créa le tube à vide qu'il baptisa audion, mais fut plus connu sous l'appelation triode.

Quelques dates :
  • 1947 : transistor à point de contact, Bell Labs
  • 1950 : transistor à jonction, Bell Labs
  • 1958 : premier circuit électronique, Texas Instruments
  • 1962 : transistor MOS, RCA
  • 1970 : première RAM statique 256 bits, 4100, Fairchild
  • 1970 : première RAM dynamique, 1024 bits, 1103, Intel
  • 1971 : le premier microprocesseur, 4004, Intel
  • 1971 : la première EPROM, 1702, Intel
  • 1972 : le premier microprocesseur 8 bits, 8008, Intel
  • 1975 : microprocesseur 6502, Synertek
  • 1976 : microprocesseur Z80, Zilog
  • 1977 : première RAM dynamique 64 ko, IBM
  • 1979 : microprocesseur 68000, Motorola
  • 1981 : première RAM dynamique 288 ko, IBM
  • 1981 : premier microprocesseur 32 bits, Hewlett packard


3.1.2 Fonctionnement du transistor

à terminer

la finesse de gravue

La finesse de gravure (logic process) joue un rôle majeur dans l'amélioration des performances des transistors et donc des circuits intégrés.


Un Wafer : galette de Silicium

Si on est capable de graver plus finement un circuit, celui-ci occupera moins de place sur un morceau de Silicium : si on divise par 2 la taille du tracé, on divise par 4 la surface occupée par le circuit.

  • on peut donc utiliser ce gain de place pour graver plus de circuits sur une même surface et donc augmenter son rendement ou ajouter de nouvelles fonctionnalités au circuit : comme l'intégration du coprocesseur au sein du 80486 en 1989.
  • en outre, un circuit dégage de la chaleur (dissipation thermique) qui est proportionnelle à la surface du circuit.
Procédé Lithographie Longueur porte Wafer (mm) Année
Px60 130 nm 70 nm 200/300 2001
P1262 90 nm 50 nm 300 2003
P1264 65 nm 35 nm 300 2005
P1266 45 nm 25 nm 300 2007
P1268 32 nm 18 nm 300 2009


Par exemple, avec une gravure en 32nm des circuits SRAM (mémoire cache), 291 Mbits sont formés par 1,9 Milliards de transistors (soit 6,22 T/bit).


Processeur Transistors Année
4004 2.300 1971
8008 3.500 1972
8080 4.500 1974
8086 29.000 1978
80286 134.000 1982
80386 275.000 1985
80486 1.200.000 1989
Pentium 3.100.000 1993
Pentium Pro 5.500.000 1995
Pentium II 7.500.000 1997
Pentium III 9.500.000 1999
Pentium 4 42.000.000 2000
Itanium 25.000.000 2001
Itanium 2 220.000.000 2002
Pentium D 376.000.000 2006
Core 2 Duo (Conroe, 65 nm) 291.000.000 2006
Core 2 Duo (Penryn, 45 nm) 410.000.000 2007
Core 2 Quad (Penryn, 45 nm) 820.000.000 2007


Processeur Millions T. Taille (mm2)
Xeon 5300 (65nm) 582 143
Core 2 Quad (Penryn, 45nm, 2007) 820 107
Amélioration +30% -25%

On April 19, 1965 Electronics Magazine published a paper by Gordon Moore in which he made a prediction about the semiconductor industry that has become the stuff of legend. Known as Moore's Law, his prediction has enabled widespread proliferation of technology worldwide, and today has become shorthand for rapid technological change. (Intel)


Loi de Moore (Source Intel)


Finesse de gravure et rentabilité

Image issue du site canardpc

Voici un petit comparatif de rentabilité entre Core 2 et Core i7 :

  • les Core 2 Penryn (Dual Core) 45nm comportent 410 millions de transistors et occupent une surface de 110mm2

  • les Core i7 Nehalem (Quad Core) 45nm comportent 731 millions de transistors et occupent une surface de 246mm2


3.1.a) le marché des semi-conducteurs en 2007

En 2007, le marché des semi-conducteurs est estimé à 271 milliards de dollars.

rang 2006 rang 2007 société % du marché
mondial
1 1 Intel 12,5
2 2 Samsung Electronics 7,4
4 3 Toshiba 4,6
3 4 Texas Instruments 4,5
8 11 Advanced Micro Devices 2,1

3.2 Algèbre de Boole et circuits numériques

algèbre de Boole
  • postscript (à visionner avec Ghostview)
  • pdf (à visionner avec Acrobat Reader)
circuits numériques
  • postscript (à visionner avec Ghostview)
  • pdf (à visionner avec Acrobat Reader)


3.3 Microprocesseur

L'invention du microprocesseur date de 1971, lorsque la société Intel réalisa le 4004.

Depuis l'apparition du Intel 4004, l'architecture n'a eu de cesse d'évoluer et nous vivons une course constante à la performance que se livrent les deux géants que sont Intel et son concurrent AMD.

La recherche de performance, si elle ne semble pas nécessaire pour les machines destinées aux particuliers, est en revanche primordiale pour l'industrie ou les domaines de recherche à la pointe du progrès, comme par exemple :

  • la prédiction météorologique
  • la simulation nucléaire
  • la conception automobile, aéronautique et spatiale
  • la bioinformatique : analyse du génome, du transcriptome
  • les films d'animation en images de synthèse

Ainsi que nous l'avons vu, l'architecture des microprocesseurs grand public repose sur l'architecture dite de Von Neumann pour laquelle l'unité de traitement des données (le microprocesseur pour nous) se décompose en deux sous unités :

  • l'ALU : unité aritmétique et logique, chargée de réaliser les calculs
  • l'UC : unité de contrôle, chargée de la traduction et l'exécution des commandes

3.3.1 Notion de registre

Afin de stocker temporairement les opérandes et les résultats des calculs, un microprocesseur est doté de registres qui sont des cellules mémoires .

Définition : registre

Un registre est un espace mémoire situé à l'intérieur du microprocesseur chargé de stocker des données temporaires qui sont généralement les opérandes ou le résultat d'un calcul de l'UAL.

Par exemple, sur l'Intel 8086, on trouve des registres d'une taille de 16 bits :

  • 8 registres principaux : AX, BX, CX, DX, BP, SP, SI, DI
  • 4 registres de segment : CS, DS, ES, SS
  • 1 registre (compteur de programme) : IP
  • 1 registre d'état : FLAGS

3.3.2 Caractéristiques

2 paramètres principaux permettent de caractériser un microprocesseur :

  • son architecture
  • sa fréquence

ces 2 facteurs conditionnent les performances de la machine qui sont mesurées :

  • soit en MIPS (million of instructions per second)
  • soit MFLOPS (million of floating point operations per second)
  • soit en secondes par l'intermédiaire d'applications spécifiques (bureautique, jeux, multimédia, ...)

Remarque : deux processeurs ayant la même fréquence de fonctionnement ne possèdent pas forcément les mêmes performances (cf suite du cours).

Voir le site www.spec.org


3.3.3 Architectures RISC et CISC

Le microprocesseur ou (Central Processing Unit en anglais) n'est en fait capable de réaliser que 3 types d'instructions :

  • LOAD R,[mem] : charger dans un registre une donnée située en mémoire à une adresse fournie en paramètre
  • STORE [mem],R :stocker une donnée contenue dans un registre dans la mémoire à une adresse fournie en paramètre
  • OPER R1,R2 : réaliser un calcul entre 2 registres

On distingue deux types d'architectures différentes pour les micrprocesseurs :

  • RISC = Reduced Instruction Set Computer (Motorola 68000, Power G4)
    dans ce type d'architecture, on utilise le format d'instruction précédent et l'adressage mémoire reste simple (i.e. qu'il n'existe que peu de manières différentes d'accèder à une cellule mémoire)

  • CISC = Complex Instruction Set Computer (Intel, AMD)
    pour ce type d'architecture, on a tendance à combiner une instruction de chargement ou de stockage avec un calcul et l'adressage mémoire peut être complexe. Par exemple l'instruction
    ADD EAX,[EBX+ECX*4+8]
    réalise plusieurs opérations :
    1. calcul de l'adresse mémoire EBX+ECX*4+8
    2. chargement de la donnée stockée sur 4 octets à l'adresse calculée
    3. addition de la donnée avec le registre EAX et écriture du résultat dans EAX

a) Historique

La conception d'un microprocesseur pose de nombreux problèmes. Plus la structure du CPU est complexe, plus les procédures de test sont longues et plus il est difficile de déterminer d'éventuels défauts de conception. Un processeur RISC, de structure moins complexe qu'un processeur CISC, est donc plus simple à mettre en oeuvre.

Plusieurs facteurs ont encouragé par le passé la conception de machines à jeu d'instruction complexe (CISC) :
  • premièrement, la lenteur de la mémoire par rapport au processeur laissait à penser qu'il était plus intéressant de soumettre au CPU des instructions complexes. Pour réaliser un traitement donné, il était préférable de définir une instruction complexe plutôt que plusieurs instructions élémentaires. De plus une instruction complexe prend alors moins de temps de chargement depuis la mémoire qu'une série d'instructions,
  • deuxièmement, le développement des langages de haut niveau (Fortran, Pascal, Ada) a posé de nombreux problèmes quant à la conception de compilateurs capables de traduire efficacement des programmes d'un langage évolué vers l'assembleur. On a donc eu tendance à incorporer au niveau processeur des instructions plus proches de la structure de ces langages.
En effet, dans les années 70 les ordinateurs utilisaient de la mémoire magnétique (réalisée à partir de tores) pour stocker les programmes. Ce type de mémoire était cher et lent. Un premier changement s'opéra avec l'arrivée des DRAM mais restait l'épineux problème du prix des DRAM :
  • en 1977, 1 Mo de DRAM coûtait $5000
  • alors qu'il ne valait plus que $6 en 1994
Le prix prohibitif des mémoires RAM et la lenteur des disques faisait qu'un code de programme était considéré comme intéressant s'il était compact.

Le processus de compilation des langages de haut niveau comme Pascal et C était lent et le code assembleur obtenu n'était pas toujours optimisé : mieux valait coder à la main.

Certains proposèrent de combler le fossé sémantique entre langage de haut niveau et assembleur afin de faciliter la tâche des programmeurs : en d'autres termes ils proposaient de faire en sorte que les instructions assembleur ressemblent aux instructions des langages de haut niveau.

On a toujours considéré que le code provenant d'un compilateur serait toujours moins performant que le code écrit à la main en assembleur par un programmeur. Cependant de nombreux progrès ont été réalisés depuis 1970.

Soit l'exemple suivant :
void swap(int t[], int k) {
    int temp = t[k];
    t[k] = t[k+1];
    t[k+1] = temp;
}

void sort(int n, int t[]) {
    int i, j;
    for (i=0; i < n; i++)
      for (j=i-1; j >= 0; --j)
        if (t[j] > t[j+1]) swap(t,j);
}

Ce morceau de code a été traduit en assembleur par un programmeur et par un compilateur C. Le code produit par le compilateur C a obtenu un meilleur résultat à l'exécution :

programmeur : 37,9s
Compilateur C : 25,3s


Au milieu des années 70, deux facteurs sont venus ébranler les idées ancrées dans les esprits par les décennies précédentes :

  • d'une part les mémoires sont devenues plus rapides qu'elles ne l'étaient auparavant,
  • d'autre part, des études réalisées par Knuth (1971), Wortman (1972) et Patterson (1982) conduites sur des langages de haut niveau montrèrent que
    • les programmes sont constitués à 85% d'affectations, d'instructions if et d'appels de procédures,
    • 80% des affectations sont de la forme variable = valeur

Les résultats précédents peuvent se résumer par la phrase suivante :80% des traitements des langages de haut niveau font appel à 20% des instructions du CPU. D'où l'idée d'améliorer la vitesse de traitement des instructions les plus souvent utilisées.


b) Architectures RISC (SPARC, Power PC, Alpha)

Des études réalisées en 1974 par John Cocke d'IBM (NY) et David Patterson 1975 montrèrent que seules 20 % des instructions d'un processeur réalisent 80 % des traitements d'un programme.

Durant les années 1980, les processeurs RISC-I et RISC-II de l'université de Berkeley virent le jour. Au début des années 1990, Apple, IBM et Motorola s'allièrent pour contrer Intel et sa prédominence sur le marché. De leur union naquit le PowerPC. On peut en outre évoquer la société MIPS qui a produit depuis 1984 des processeurs à architecture RISC ainsi que Sun Microsystems avec son SPARC (Scalable Processor ARChitecture).

Le concept RISC consiste à créer un jeu d'instructions simples mais très rapides. L'accès à la mémoire est simplifié grâce à l'utilisation de deux instructions (LOAD et STORE).

On peut ainsi :

  • développer et tester un microprocesseur plus rapidement
  • monter plus haut en fréquence de fonctionnement
  • utiliser moins de transistors pour les décodeurs et obtenir des circuits plus petits.
  • la place libérée est utilisée pour augmenter le nombre de registres et la taille des caches.

c) Architectures CISC (Intel, AMD)

Par opposition au RISC, l'architecture CISC désigne des microprocesseurs disposant d'un jeu d'instructions autorisant différents types d'accès aux données. Le but est ici de "coller" au plus près à la syntaxe des langages de programmation de haut niveau en fournissant des instructions proches de celles de ces langages.

Sur le tableau ci-dessous, on tente de donner une idée de la différence CISC/RISC. On utilise dans les 2 cas le langage assembleur de l'Intel 8086. On déplace sur cet exemple 100 octets de l'adresse mémoire src vers l'adresse mémoire dst. A gauche on utilise une macro-instruction (CISC) REP MOVSB équivalente à la partie figurée en vert à droite. (Note : le préfixe REP peut être accolé à certaines instructions afin de les utiliser en boucle).

CISC-like RISC-like Pascal
MOV CX,100
MOV DI,dst
MOV SI,src
REP MOVSB






MOV CX,100
MOV DI,dst
MOV SI,src
boucle:
MOV AL,[SI]
MOV [DI],AL
INC SI
INC DI
DEC CX
JNZ boucle
Var
  i : integer;
  src, dst : array[1..100] of byte;

for i:=1 to 100 do
  dst[i]=src[i];


d) Comparaison RISC / CISC

Chaque architecture possède des avantages et des inconvénients :

  • pour le RISC, la complexité est reportée au niveau du compilateur,
  • pour le CISC le décodage est plus pénalisant car les instructions sont complexes et de longueur variable, alors que pour une architecture RISC les instructions sont de longueur fixe.
En fait les processeur CISC se sont orientées progressivement vers une architecture RISC. Les instructions CISC sont traduites en micro-instructions RISC traitées par le coeur du processeur.


3.3.4 Langage machine / langage assembleur

Le langage machine est le langage compris par le microprocesseur. Il existe différents langages machines qui dépendent du type de microprocesseur. Ce langage est difficile à maitriser puisque chaque instruction est codée par une séquence propre de bits. Afin de faciliter la tâche du programmeur, on a créé le langage assembleur qui utilise des mnémoniques pour le codage des instructions :




Langage
machine
Langage
assembleur
Pascal
A1 00 01
8B 1E 02 01
01 D8
A3 04 01
MOV AX,[100h]
MOV BX,[102h]
ADD AX,BX
MOV [104h],AX
var a,b,c : integer;

c:=a+b;



3.3.5 Langage assembleur des Intel 80x86/Pentium

Dans cette section, nous allons donner un aperçu du langage assembleur des microprocesseurs Intel de la famille 80x86 afin de pouvoir utiliser ce langage par la suite.

Le Pentium, qui est dérivé du 80486, est un microprocesseur CISC capable d'exécuter un programme dans différents modes de fonctionnement :

  • le mode protégé multitâche permet d'utiliser toute la puissance du processeur. Les registres sont des registres 32 bits.
  • le mode virtuel permet à des applications 8086 (environnement DOS) de s'exécuter dans le mode protégé,
  • le mode réel correspond au fonctionnement d'un 8086. Les registres 32 bits (EAX, ...) ne sont pas disponibles, on utilise des registres de 16 bits (AX, ...) et on ne peut accéder qu'à 1 Mo de mémoire.


a) Registres

Il existe 8 registres généraux (General Purpose Registers) :

  • en 16 bits : AX, BX, CX, DX, DI, SI, BP, SP
  • en 32 bits : EAX, EBX, ECX, EDX, EDI, ESI, EBP, ESP dont la partie basse (16 premiers bits) peut être manipulée séparément de la partie haute, on utilise alors les dénominations AX, BX, CX, DX, DI, SI, BP, SP.
  • en 64 bits : RAX, RBX, RCX, RDX, RDI, RSI, RBP, RSP dont la partie basse (16 premiers bits) peut être manipulée séparément de la partie haute (cf 32 bits).

Parmi ces registres on distingue :

  • les registres de données : EAX, EBX, ECX, EDX dont la partie basse (les 16 premiers bits) se subdivise en 2 sous registres de 8 bits l'un appelé partie haute (H) et l'autre appelé partie basse (L). Par exemple : AX de décompose en AL, AH.
    • EAX (accumulateur) est utilisé pour les multiplications et divisions, c'est le plus rapide pour la réalisation d'opérations arithmétiques et logiques,
    • EBX est utilisé comme opérande ou comme registre pointeur (cf. ci-après),
    • ECX (compteur) est utilisé comme compteur dans les opérations itératives comme le transfert de données (LODSB, STOSB),
    • EDX est utilisé pour les multiplications et divisions ainsi que l'accès aux circuits d'entrées et sorties.
  • les registres pointeurs et index :
    • EIP (Instruction Pointer) représente le pointeur d'instruction, il est non modifiable par le programmeur. Seules les instructions comme CALL, RET, JMP, JNC, ... peuvent le modifier,
    • ESP (Stack Pointer) est le pointeur de pile
    • EBP (Base Pointer) est généralement utilisé par le programmeur pour faire référence aux paramètres des procédures et fonctions qui sont passés dans la pile
    • ESI (Source Index) permet de faire référence à la mémoire
    • EDI (Destination Index) permet de faire référence à la mémoire


  • les registre de segments qui combinés aux registres pointeurs et index permettent d'adresser les données.
    • CS (Code Segment) : segment courant du code (CS:EIP contient l'adresse de la prochaine instruction à exécuter),
    • DS (Data Segment) : segment courant des données,
    • SS (Stack Segment) : segment courant de la pile,
    • ES (Extra Segment) : segment additionnel,
    • FS (Extra Segment) : segment additionnel,
    • GS (Extra Segment) : segment additionnel.
    Les registres segments sont composés d'une partie visible de 16 bits et d'une partie cachée comprenant des informations relatives à la taille du segment, son type et ses droits d'accès.

  • un registre d'état EFLAGS de 32 bits comprenant des indicateurs (status flags) chacun codé sur un bit :
    • AF (Auxiliary Flag) : indicateur de retenue auxiliaire, mis à 1 lorsqu'il y a une retenue du quartet de poids faible dans le quartet de poids fort,
    • CF (Carry Flag) : indicateur de retenue, mis à 1 lorsqu'un calcul produit une retenue sur 8 ou 16 bits,
    • OF (Overflow Flag) : indicateur de débordement indiquant que l'on a dépassé les possibilités de stockage et qu'un bit significatif a été perdu,
    • SF (Sign Flag) : indicateur de signe, utilisé pour les opérations sur les nombres signés,
    • PF (Parity Flag) : indicateur de parité, mis à 1 si le résultat d'une opération contient un nombre pair de 1,
    • ZF (Zéro Flag) : indicateur de 0, mis à 1 quand le résultat d'une opération est 0.

Registres du Pentium 4 avec architecture 64 bits
(image issue du site de Pierre Marchand, Université de Laval, Canada)

b) Adresse mémoire

Le calcul d'une adresse mémoire physique dépend du mode de fonctionnement du processeur.

  • en mode protégé une adresse s'exprime sur 32 bits ce qui permet d'adresser jusqu'à 4 Go. Nous n'expliquerons pas le calcul de l'adresse dans ce mode.
  • en mode réel (mode du 8086), les registres de données sont sur 16 bits et les adresses s'expriment sur 20 bits (1 Mo).

Pour solutionner ce problème on utilise une technique appelée ségmentation qui consiste à combiner deux registres de 16 bits pour obtenir une adresse sur 20 bits suivant la formule :

adresse 20 bits = registre segment × 16 + offset

L'offset peut être une valeur numérique, un registre ou une combinaison des deux. Toutes les combinaisons entre registre segment et registre index ne sont pas possibles :

Registre
Segment
Registre
Pointeur
Spécification
CSIPCompteur ordinal
SSSPSommet de pile
SSBPAccès à la pile
DSSISource pour LODSB/W
ESDIDestination pour STOSB/W
DSBX 
Exemples de combinaisons segment, pointeur


Pour être plus exact, la description de l'offset en mode protégé est donnée par :

Offset = Base + Index × Echelle + deplacement

avec :

  • base est un registre général 32 bits ou une adresse mémoire,
  • la partie Index × Echelle est facultative : Index est un registre général 32 bits (excepté ESP) multiplié par un facteur d'Echelle qui est de 1, 2, 4 ou 8. Il est de 1 par défaut,
  • la partie deplacement est facultative : deplacement est une constante de 8,16 ou 32 bits




3.3.6 Types de données
Dénomination Qté en bits Codage Assembleur
bit 1 DBIT
byte (octet) 8 DB
word (mot) 16 DW
double word 32 DD
quad word 64 DQ
Types de données manipulées en assembleur

Le Pentium est capable de traiter des quantités allant jusqu'à 64 bits, soit deux doubles mots de 32 bits, voire jusqu'à 80 bits pour les réels (au niveau du coprocesseur).




a) Taille des instructions

La taille des instructions varie de 1 à plusieurs octets, par exemple :

Taille (en bits) 8 16/32
Instruction CALL adresse
 
Taille (en bits) 5 3
Instruction PUSH Reg



b) Ecriture d'un programme Assembleur

Il existe conventionnellement dans les programmes élémentaires écrits en assembleur entre 2 à 3 segments :

  • le segment de pile pour lequel on indique la taille de la pile,
  • le segment de données dans lequel on définit les variables,
  • le segment de code qui contient le code du programme.

Un programme assembleur aura donc la structure suivante (cas MASM/TASM) :

STACK SEGMENT 4096 ; Pile de 4096 octets

DATA SEGMENT PUBLIC
  var0  DB ?
  var1  DW 1234
  var2  DD ?
  ...
DATA ENDS

CODE SEGMENT PUBLIC
     ASSUME DS:DATA, CS:CODE

     MOV  AX,var0
     ...
CODE ENDS


Pour un programme écrit en NASM, la syntaxe diffère :

; GLOBAL indique que main est un sous-programme visible des autres fichiers objets
global main
; EXTERN indique que les sous-programmes existent dans d'autres fichiers objets
extern printf
extern scanf

; ========================
; ===== DATA SECTION =====
; ========================
section .data 

msg_1:   db 'la somme des éléments du tableau est %d',10,0
msg_2:   db 't[%d]=%d',10,0

taille EQU 20
tableau: times taille dd 0

; COMMON indique l'existence d'une données dans un autre fichier objet
; qui occupe 4 octets
common n 4

; ========================
; ===== CODE SECTION =====
; ========================
section .text 

main:
    push ebp
    mov  ebp,esp
    ...


d) Instructions

Dans la suite de cette section nous décrivons les instructions de bases pour écrire des programmes simples en assembleur.


α) Instruction de transfert des données : MOV

Les instructions de transfert permettent l'affectation de valeurs entre registres et mémoire.

  • MOV dst, src
    l'instruction MOV attribue la valeur de la source à l'opérande de destination. src peut également être une valeur constante,
  • MOVSX dst, src
    cette instruction se comporte comme MOV sauf qu'elle permet de convertir une valeur de 8 en 16 bits ou de 16 en 32 bits en une valeur signée,
  • MOVZX dst, src
    cette instruction se comporte comme MOV sauf qu'elle permet de convertir une valeur de 8 en 16 bits ou de 16 en 32 bits en une valeur non signée,

Exemples :

  • MOV AH,BL (mettre la valeur de BL dans AH)
  • MOV AX,100 (mettre la valeur constante 100 dans AX)
  • MOV EAX,[1000] (mettre dans EAX la valeur codée sur 32 bits stockée à l'adresse 1000 en mémoire)
  • MOV EDX,[EBX+ESI*4+8]
  • MOVZX EAX,BYTE [ESI]MOV EAX,0 et MOV AL,BYTE [ESI]


β) Instructions arithmétiques : ADD, SUB, INC, SEC, NEG, MUL, DIV

Les instructions arithmétiques sont les suivantes :

  • ADD dst, src
    réalise l'addition de dst avec src et dépose le résultat dans dst
  • SUB dst, src
    réalise la soustraction de dst avec src et dépose le résultat dans dst
  • INC dst
    incrémente la valeur pointée par dst
  • DEC dst
    décrémente la valeur
  • NEG dst réalise le complément à 2 de la valeur pointée par dst
  • MUL, IMUL src
    MUL réalise une multiplication non signée entre l'accumulateur et un autre registre ou une valeur, alors que IMUL réalise une multiplication signée. En fonction de la taille des données on obtiendra le résultat dans des registres différents :
    • AL × 8 bits -> AX
    • AX × 16 bits -> DX:AX
    • EAX × 32 bits -> EDX:EAX
  • DIV, IDIV src
    DIV opère une division non sign\'ee entre l'accumulateur et un autre registre ou une valeur, alors que IDIV réalise une division signée. En fonction de la taille des données on obtiendra le résultat dans des registres différents :
    • AX / 8 bits -> AL quotient, AH reste
    • DX:AX / 16 bits -> AX quotient, DX reste
    • EDX:EAX / 32 bits -> EAX quotient, EDX reste

Exemples :

  • ADD AX,BX (ajouter la valeur de BX à AX)
  • SUB EBX,100 (soustraire la valeur constante 100 à EBX)
  • INC DWORD [EBX] (incrémenter la valeur 32 bits stockée en mémoire à l'adresse stockée dans EBX)
  • MUL ECX (multiplier EAX par la valeur stockée dans ECX)
  • DIV WORD [EBX+ESI*2] (diviser DX:AX par la valeur sur 16 bits stockées en mémoire à l'adresse obtenue par le calcul de EBX+ESI*2)


γ) Instructions logiques : AND, OR, NOT, XOR

  • AND dst, src
    réalise le ET logique entre les opérandes
  • OR dst, src
    réalise le OU logique
  • XOR dst, src
    réalise le OU-exclusif logique
  • NOT dst
    réalise le complément

δ) Instructions de comparaison : CMP, TEST

  • CMP dst, src
    réalise la comparaison de deux valeurs. On effectue en fait une soustraction entre les 2 valeurs mais seuls les bits du registres EFLAGS sont positionnés en conséquence.
  • TEST dst, src
    réalise la comparaison de deux valeurs. On effectue en fait un ET logique entre les 2 valeurs mais seuls les bits du registres EFLAGS sont positionnés en conséquence.

Exemples :

  • CMP AL,CH
  • CMP EBX,100
  • TEST EAX,[EBX]



ι) Instructions de saut et de branchement : CALL, RET, JMP, JE, JNE, JL, JLE, JG, JGE

  • JMP adr
    saut à l'adresse adr,
  • CALL adr
    appel de sous-programme,
  • RET
    retour de sous-programme,
  • JE,JZ adr
    Jump On Equal, ou Jump on Zero, correspond au cas ZF = 1
  • JG adr
    Jump on Greater Than, SF = OF et ZF = 0
  • JGE adr
    Jump on Greater or Equal, SF = OF
  • JL adr
    Jump on Less, SF != OF
  • JLE adr
    Jump on Less or Equal, SF != OF ou ZF = 1
Instruction Signification Signé (S) ou non (U)
ja Jump if above U
jae Jump if above or Equal U
jb Jump if below U
jbe Jump if below or Equal U
jc Jump if Carry  
jcxz Jump if CX is Zero  
je Jump if Equal  
jecxz Jump if ECX is Zero  
jz Jump if Zero  
jg Jump if greater S
jge Jump if greater or Equal S
jl Jump if less S
jle Jump if less or Equal S
jmp Unconditional jump  
jna Jump Not above U
jnae Jump Not above or Equal U
jnc Jump if Not Carry  
jncxz Jump if CX Not Zero  
jne Jump if Not Equal  
jng Jump if Not greater S
jnge Jump if Not greater or Equal S
jnl Jump if Not less S
jnle Jump if Not less or Equal S
jno Jump if Not Overflow  
jnp Jump if Not Parity  
jns Jump if Not signed  
jnz Jump if Not Zero  
jo Jump if Overflow  
jp Jump if Parity  
jpe Jump if Parity Even  
jpo Jump if Parity Odd  
js Jump if signed  
jz Jump if Zero  

e) Spécificités de nasm

documentation de nasm : documentation

On peut créer des macro-commandes qui permettent de rendre l'écriture d'un programme moins pénible :


f) désassemblage sous Linux

Désassembler un programme consiste à obtenir son code source assembleur à partir de son exécutable.

Pour désassembler un programme sous Linux il faut utiliser la commande objdump :

objdump -d -M intel -r -S -l --no-show-raw-insn -j .text <monexe>
  • -d : désassemble
  • -M intel : affiche le code au format Intel
  • -r : relocation
  • -S : affiche une partie du code source si possible
  • -l : numéro de ligne du source
  • --no-show-raw-insn : ne pas afficher les octets qui codent les instructions
  • -j name : nom de la section à désassembler

Pour vous simplifier la vie, créer un alias dans votre fichier ~/.bashrc :

alias myobjdump='objdump -d -M intel -r -S -l --no-show-raw-insn -j .text'


3.3.7 Appel de sous-programmes, passage de paramètres et variables locales

Le passage de paramètres à des procédures ou des fonctions est effectué au travers de la pile. Les variables locales sont également allouées dans la pile. Lors de l'appel d'un sous-programme on réalise les opérations suivantes :

  1. on commence par empiler les paramètres du sous-programme sur la pile,
  2. on empile l'adresse CS:EIP de la prochaine instruction à exécuter après appel du sous-programme,
  3. on modifie CS:EIP pour lui donner l'adresse du sous-programme,
  4. on exécute le sous-programme jusqu'à atteindre une instruction RET,
  5. avec RET, on dépile l'adresse de la prochaine instruction à exécuter et on l'affecte à CS:EIP
int sum( int a, int b ) { // sous-programme appelé
  int r;
  r = a + b;
  return r;
}

int main( ) { // sous-programme appelant
  int res;
  res = sum(1,2);
}

A l'intérieur d'un sous-programme on utilise généralement la pile afin d'allouer les variables locales. Afin de faciliter l'accès aux paramètres et aux variables locales on utilise le registre EBP.

Adresse Code Instructions (main)
08048A20 68 02 00 00 00PUSH DWORD 2
08048A25 B8 01 00 00 00 MOV EAX,1
08048A2A 50PUSH EAX
08048A2B E8 23 00 00 00 CALL 08048A53
08048A30 ... ...

Adresse Code Instructions (sum)
08048A53 55 PUSH EBP
08048A54 89 E5 MOV EBP,ESP
08048A56 81 EC 04 00 00 00 SUB ESP,4    ; (variable r, équivalent à PUSH EAX)
08048A5C 8B 45 08 MOV EAX,[EBP+8]
08048A5F 03 45 0C ADD EAX,[EBP+12]
08048A62 89 45 FC MOV DWORD [EBP-4],EAX
08048A65 8B 45 FC MOV EAX,DWORD [EBP-4]
08048A68 89 EC MOV ESP,EBP
08048A6A 5D POP EBP
08048A6B C3 RET

Voici l'état de la pile une fois à l'intérieur de la fonction sum :

ESP Valeur (hexa) EBP Indication
ESP-4 00.00.00.02 EBP+12 second paramètre
ESP-8 00.00.00.01 EBP+8 premier paramètre
ESP-12 08.04.8A.30 EBP+4 adresse de retour du sous-programme
ESP-16 ??.??.??.?? EBP+0 ancienne valeur de EBP
ESP-20 ??.??.??.?? EBP-4 variable temporaire r

La pile possède une taille maximale : 4, 8 ou 16 ko en général. Au début le pointeur de pile ESP prend la taille maximale de la pile. Lorsque l'on empile une valeur on décrémente ESP. Les instructions PUSH et POP ont donc le comportement suivant :

  • PUSH EAX est équivalent à ESP = ESP - 4; Mem[SS:ESP] = EAX,
  • POP EAX est équivalent à EAX = Mem[SS:ESP]; ESP = ESP + 4.

Lors du retour de sous-programme les paramètres sont toujours présents dans la pile, il faut donc les supprimer. Il existe ici deux manières de procéder :

  • suppression par l'appelant : (cas du langage C)
    c'est le sous-programme appelant (main) qui supprime les paramètres : dans ce cas, après l'appel de sum on ajoute l'instruction ADD ESP,8,
  • suppression par l'appelé : (cas du langage Pascal)
    c'est le sous-programme appelé (sum) qui supprime les paramètres : dans ce cas on utilise l'instruction RET avec un paramètre : RET 8.

Enfin, lors de l'appel de fonctions, on peut utiliser la pile pour passer la valeur de retour de la fonction ou un registre comme c'est le cas dans l'exemple précédent.


Question : pourquoi le langage C utilise t-il une suppression des paramètres par l'appelant ?


conventions d'appel

La convention d'appel d'un sous-programme (calling convention) détermine la manière dont les paramètres sont empilés :

  • en général, on commencera par empiler le dernier paramètre (cas du langage C),
  • il se peut également que l'on commence par empiler le premier paramètre (cas du langage Pascal)
  • cdecl : la procédure appelante qui supprimera les paramètres mis dans la pile (option __attribute__((cdecl)) de gcc)
  • stdcall : la fonction appelée supprime les paramètres de la pile (option __attribute__((stdcall)) de gcc)
  • fastcall : utilisation des registres EAX, EDX, ECX au lieu de passer les paramètres dans la pile (option __attribute__((fastcall)) de gcc)

En outre, en mode 32 bits (STDCALL), on s'attend à ce que les registres EBP, EBX, EDI et ESI ne soient pas modifiés lors de l'appel d'un sous-programme. Cela implique que le sous-programme devra sauvegarder les valeurs de ces registres temporairement dans la pile, puis les restaurer avant de quitter le sous-programme.

En mode 64 bits (FASTCALL) les paramètres sont passés dans les registres RCX, RDX, R8, R9. S'il y a plus de 4 paramètres, les paramètres restants seront passés dans la pile. Les registres suivants ne doivent pas être modifiés lors de l'appel d'un sous-programme : RBP, RBX, RDI, RSI, R2 à R15, xmm6 à xmm15



3.4 Améliorations des microprocesseurs


3.4.1 Généralités

Le temps d'exécution d'un programme est donné par la formule suivante :

Texec = Nins × CPI × Tcycle


  • Texec : temps d'exécution,
  • Nins : nombre d'instructions,
  • CPI : nombre de cycles (moyen) par instructions,
  • Tcycle : temps de cycle (ns)

Les différentes évolutions des ordinateurs ont pour but de diminuer le temps d'exécution des programmes.

  • la première amélioration consiste à diminuer le temps de cycle pour cela il suffit d'augmenter la fréquence des processeurs. Grossièrement, un processeur à 3 Ghz fonctionne 3 fois plus vite qu'un processeur à 1 Ghz.
  • on peut ensuite diminuer le nombre d'instructions ou le nombre de cycles par instructions. Or dans ce cas, il semble que le produit Nins × CPI reste constant :
    • en effet si on diminue le nombre d'instructions on crée des instructions plus complexes (CISC) qui nécessitent plus de cycles pour être exécutées.
    • si par contre on diminue le nombre de cycles par instructions on crée des instructions simples (RISC) et il faut utiliser plusieurs instructions pour réaliser le même traitement qu'une instruction CISC.

Il a donc fallu élaborer des solutions capables de diminuer le temps nécessaire au traitement des instructions qu'elles soient CISC ou RISC.

Les architectures des processeurs modernes tentent de maximiser l'ILP (Instruction Level Parallelism) d'un flux d'instructions : cela signifie que l'on tente d'exécuter le plus d'instructions en parallèle.


3.4.2 Traitement des instructions

Le traitement des instructions passe par 5 étapes fondamentales :

FETCH Il s'agit de l'étape de chargement depuis la mémoire de la prochaine instruction à exécuter
DECODE L'instruction est ensuite décodée et traduite pour être interprétée
LOAD OPERAND si l'instruction nécessite des données en provenance de la mémoire ou de registres, on envoie une requête afin de les récupérer
EXECUTE L'instruction est ensuite exécutée par l'UAL s'il s'agit d'une opération arithmétique ou logique
RESULT
WRITE BACK
Le résultat est mis à jour dans un registre ou en mémoire



Lors de l'exécution d'une instruction, on attend d'avoir effectué les 5 étapes de traitement avant de passer au traitement de l'instruction suivante.


3.4.3 Pipelining (amélioration sur la longueur)

a) principe

Le but du pipeline consiste à diminuer le temps d'attente entre le traitement de chaque instruction. Plutôt que de disposer d'une unité de traitement qui réalise les 5 étapes précédentes, on décompose l'unité de traitement en 5 sous-unités. Les 80486 sont les premiers microprocesseurs de la gamme Intel à utiliser le pipeline.

Dès que la première instruction a terminé l'étape FETCH, elle passe dans la sous-unité DECODE. Dans le même temps, l'instruction suivante passe dans l'étape FETCH.

Il est préférable que chaque étpape de traitement associée à un étage du pipeline s'exécute en un cycle d'horloge. Si ce n'est pas le cas, on décompose une étape en plusieurs sous-étapes et on allonge ainsi la longueur du pipeline.

Comme on peut le voir sur le schéma précédent, le gain est intéressant puisque l'instruction 5 se termine au bout de 9 cycles d'horloge alors que dans un modèle d'exécution sans pipeline elle serait exécutée au bout de 25 cycles.

Le gain apporté par un pipeline de k étages est donné par le calcul suivant :

  • temps d'exécution sans pipeline pour n instructions : n × k
  • temps d'exécution avec pipeline : k + (n-1)

Le gain est donné par le rapport (n × k) / (k + n - 1) soit k.




Exemples de pipelines pour architectures P6 et Netburst

L'autre intéret du pipeline est qu'il permet l'amélioration des performances avec la montée en fréquence. On pourra lire à ce sujet l'excellent article de Franck Delattre et Marc Prieur sur Hardware.fr intitulé : Intel Core Duo 2

  • 20 étages avec Pentium 4 Willamette et Northwood
  • 31 étages avec Pentium 4 Prescott et Cedar Mill
  • 45 étages prévus avec Pentium 4 Téjas (finalement abandonné)
  • 14 étages avec Pentium M
  • 12 étages avec Core 2 Duo

b) problèmes liés au pipeline : dépendances


I1 C:=A+B;
I2 D:=C*3;
I3 E:=sin(A)
L'exécution de l'instruction I2 nécessite que l'instruction I1 ait été réalisée. Cela implique au niveau du pipeline des états d'attente (stall).


Certains mécanismes permettent cependant de remédier à ce genre de problème (réordonnancement des instructions ou forward after execute).

c) problèmes liés au pipeline : branchements conditionnels

Exemple 1   Exemple 2
c := ...
if (c=0) then
   b:=0
else
   b:=a+c;
a:=a/2;
c:=1;
a:=0;
while (c<100) do
begin
 a:=a+c;
end;
  if:
  CMP CX,0
  JNZ else
  MOV BX,0
  JMP end_if
else:
  MOV BX,AX
  ADD BX,CX
end_if:
  SHR AX,1

Les instructions de branchement obligent quant à elles à vider le pipeline.

  MOV AX,0
  MOV CX,1
while:
  CMP CX,100
  JGE end_while
  ADD AX,CX
  INC CX
  JMP while
end_while:
Si le résultat de la comparaison de CX avec la valeur 100 est vrai alors on doit déplacer le pointeur d'instruction (IP) vers end_while et vider le pipeline qui contient les instructions ADD AX,CX et INC CX.

Vider le pipeline prend beaucoup de temps d'autant plus que le pipeline possède un nombre important d'étages. Pour remédier à ce genre de problème on utilise un mécanisme appelé prédiction de banchement qui a pour but de recenser lors de branchements le comportement le plus probable.


Les mécanismes de prédiction de branchement permettent d'atteindre une fiabilité de prédiction de l'ordre de 90 à 95 %.



3.4.5 microprocesseurs superscalaires (amélioration sur la largeur)

Une autre amélioration possible consiste à disposer de plusieurs unités d'exécutions. Si on dispose de n unités d'exécution, on divise théoriquement le temps d'exécution par n.

Cependant, disposer d'une batterie d'unités de traitement (ou d'exécution) n'est pas rentable si on ne peut occuper à plein régime chacunes de ces unités.


3.4.6 Combiner superscalaires et pipeline

Il s'agit ici de trouver un compromis entre amélioration en longueur et amélioration en largeur. Deux modèles semblent se détacher :

  • utiliser de nombreux pipeline avec peu d'étages
  • utiliser peu de pipelines avec de nombreux étages


3.4.7a Coprocesseur

Le coprocesseur est un circuit dédié au calcul des nombres flottants (rééls). Du 8086 au 80486SX, les microprocesseurs Intel pouvaient utiliser un coprocesseur externe. A partir du 80486DX, le coprocesseur a été intégré au niveau du die.

  • les coprocesseurs Intel sont composés de 8 registres (ST0 à ST7) de 80 bits que l'on utilise comme une pile (voire une file)
  • le coprocesseur comporte un registre de status qui indique les erreurs éventuelles.
  • les nombres sont chargés au niveau des registres grâce à des instructions spécifiques :
  • les instructions liées au coprocesseur commencent par la lettre F

Chargement :

  • FLD src : charge un nombre en virgule flottante depuis la mémoire et le stocke au sommet de la pile
  • FILD src : de même, avec un nombre entier qui est convertit en virgule flottante
  • FLDZ : charge la valeur 0
  • FLD1 : charge la valeur 1

Stockage :

  • FST dest : stocke le sommet de pile ST0 en mémoire ou dans un autre registre du coprocesseur
  • FSTP dest : agit comme FST mais la valeur en sommet de pile est dépilée
  • FIST : comme FST mais convertit le nombre en virgule flottante en un entier
  • FISTP dest : comme FSTP pour les entiers

Manipulation de la pile :

  • FXCH STn : échange les valeurs de ST0 et STn
  • FFREE STn : libère un registre de la pile en le marquant comme inutilisé

Addition :

  • FADD src : ST0 += src, ou src est un nombre en mémoire ou un autre registre STn
  • FADD STn, ST0 : STn += ST0,
  • FADDP STn : dst += ST0, puis ST0 est dépilé
  • FADDP STn, ST0 : idem à l'instruction précédente
  • FIADD src : ST0 += (float) src, ou src est un entier

Soustraction, multiplication, division :

on retrouve le même schéma pour les opérations de base :

  • soustraction : FSUB
  • multiplication : FMUL
  • division : FDIV

Comparaison :

  • FCOM src : compare ST0 à src
  • FCOMP src : compare ST0 à src, puis dépile ST0
  • FCOMPP : compare ST0 à ST1 puis dépile ST0 et ST1 :
  • FICOM src : compare ST0 à l'entier stocké par src :
  • FICOMP src : idem à la précédente instruction et dépile ST0
  • FTST : compare ST0 à 0

Les instructions de comparaison changent les bits de statut du coprocesseur mais ceux-ci ne sont pas accessibles directement. Il faut transférer ces bits vers les bits du registre EFLAGS comme suit :


;   if (x > y) ...
;
    fld    qword [x]
    fcomp  qword [y]
    fstsw  ax         ; place les bits Z,C,F dans AX
    sahf              ; stocke AH dans EFLAGS
    jna    else       ; pour comparaison de nombres non signés
then:
    ; code du then
    jmp    end_if
else:
    ; code du else
end_if:

A partir du Pentium Pro, de nouvelles instructions qui modifient directement le registre EFLAGS ont été ajoutées :

  • FCOMI STn : compare ST0 à STn
  • FCOMIP src : idem, puis dépile ST0

Autres fonctions :

  • changement de signe : FCHS : ST0 = - ST0
  • sinus, cosinus, tangente : FCOS, FSIN, FPTAN
  • addition, division, ... : FADD, FSUB, FDIV, FMUL
  • valeurs absolue, racine carrée : FABS, FSQRT

Exemples :


section .text
(1) FLD 	dword [deux]
(2) FLD		dword [trois]
(3) FDIV	st1
opération st0 st1 st2 ... st7
(1) 2 0 0 ... 0
(2) 3 2 0 ... 0
(3) 1.5 2 0 ... 0

section .text
(1) FLD 	dword [deux]
(2) FLD		dword [trois]
(3) FDIV	st1,st0
opération st0 st1 st2 ... st7
(1) 2 0 0 ... 0
(2) 3 2 0 ... 0
(3) 3 0.666 0 ... 0

section .text
(1) FLD 	dword [deux]
(2) FLD		dword [trois]
(3) FDIVP	st1,st0       ;  fdivP
opération st0 st1 st2 ... st7
(1) 2 0 0 ... 0
(2) 3 2 0 ... 3
(3) 0.66 0 0 ... 3

section .text
(1) FLD 	dword [deux]
(2) FLD		dword [trois]
(3) FDIV	st0,st1
opération st0 st1 st2 ... st7
(1) 2 0 0 ... 0
(2) 3 2 0 ... 3
(3) 1.5 2 0 ... 0


3.4.7b Processeurs vectoriels

Principe des processeurs vectoriels (Intel)

Les processeurs vectoriels utilisent une architecture qualifiée de SIMD (Single Instruction Multiple Data). Il sont capables d'effectuer la même opération sur plusieurs données différentes en même temps.

Les processeurs actuels intègrent des unités de calcul vectoriel dédiées au multimédia :

  • MMX (Multi-Media eXtension) intégrée au Pentium MMX (registres 64 bits, entiers)
  • 3D Now ! version MMX/SSE des processeurs AMD apparue sur les K6-II (64 bits)
  • SSE (Streaming Simd Extension) apparu sur les Pentium III (128 bits, entiers ou flottants 32 bits)
  • SSE2 évolution du SSE sur le Pentium 4 (flottants 64 bits)

cas des unités MMX

Les unités MMX comprennent, 8 registres de 64 bits (mm0 à mm7), ces registres permettent de manipuler les données sous forme :

  • de 1 valeur de 64 bits
  • ou de 2 valeurs de 32 bits
  • ou de 4 valeurs de 16 bits
  • ou de 8 valeurs de 8 bits

cas des unités SSE


Historique SSE Intel (Intel)


Principe des processeurs vectoriels (Intel)

Les unités SSE comprennent, en mode 32 bits, 8 registres de 128 bits (xmm0 à xmm7), ces registres permettent de manipuler les données sous forme :

  • de 2 valeurs de 64 bits
  • ou de 4 valeurs de 32 bits
  • ou de 8 valeurs de 16 bits
  • ou de 16 valeurs de 8 bits

Exemples de quelques instructions :

  • movdqu xmmDst/128,xmmSrc,m128 move unaligned data
  • movdqa xmmDst/128,xmmSrc,m128 move aligned data
  • pand xmmDst,xmmSrc/m128 packed and
  • paddb/paddw/paddd/paddq xmmDst,xmmSrc/m128 packed integer addition

La plupart des compilateurs C possèdent une bibliothèque <xmmintrin.h> qui permet d'utiliser des instructions liées aux unités SSE.

  • __m128 définition d'un registre 128 bits
  • _mm_malloc(size,align) permet d'allouer de la mémoire en l'alignant par rapport à une valeur
  • _mm_free(adr) libère la mémoire allouée par _mm_malloc
  • _mm_and_ps(xmm1,xmm2) utilisant du packed and

Exemple : SSE

Liens concernant le SSE :


3.4.8 Exécution dans le désordre (Out-of-Order OOO)

Les microprocesseurs actuels exécutent les instructions dans un mode dit "dans le désordre" (out of order).

Les instructions x86 (appelées macro-opérations) sont lues depuis le compteur ordinal (CS:EIP) dans un ordre précis. Elles sont ensuite décodées et traduites en une à plusieurs micro-opérations qui peuvent être exécutées dans un ordre différent de celui des instructions x86 auxquelles elles correspondent. Il faut ensuite faire un sorte de retirer (retirement ou result write back) les instructions dans l'ordre initial.

Ce genre de technique permet d'accélérer le traitement des instructions en tentant de minimiser les états d'attentes, mais pose également de nombreux problèmes à résoudre.

Les instructions sont chargées (FETCH) dans l'ordre puis décodées. Elles sont alors :

  • stockées dans un pool d'instructions (ROB ou ReOrder Buffer) qui a pour but de garantir qu'au final, le résultat des instructions correspond à l'ordre de leur entrée,
  • un autre mécanisme (RS Reservation Station) a pour but de traiter les instructions dès que les ressources sur lesquelles elles agissent sont disponibles.

Le cheminement d'une instruction est donc le suivant :

  • entrée dans le ROB (40 instructions)
  • traitement par la RS dès que possible
  • retour dans le ROB
  • mise à jour du résultat (retirement)

Un autre point important concerne une technique appelée Register Renaming utilisée pour que le mécanisme précédent fonctionne. Les microprocesseurs x86 jusqu'au Pentium 4 disposent uniquement de 8 registres généraux. Pour que l'exécution des instructions du ROB soit possible, le Pentium dispose en interne de plusieurs registres qui sont des "copies" des registres généraux.


3.4.9 La technologie VLIW / EPIC

L'approche VLIW (pour Very Long Instruction Word, EPIC chez Intel) est une approche prometteuse mais qui n'a pas connu pour le moment le succès attendu. Elle a été mise en oeuvre au sein du processeur Itanium d'Intel.

Cette technique consiste a coder plusieurs instructions dans une seule grande instruction de manière à utiliser au mieux les unités d'exécution.

Par exemple pour l'Itanium, les instructions sont codées sur 128 bits :

Instruction 2
41 bits
Instruction 1
41 bits
Instruction 0
41 bits
Gabarit
5 bits

Intel a présenté, le 1er Novembre 2007, 7 nouveaux processeurs Itanium. Les modèles de la série 9100 se caractérisent par une architecture double coeur cadencés à 1,60 GHz (modèles N) et 1,66 GHz (série M). La taille du cache L3 varie de 8 Mo (9130M) à 24 Mo (9150N et M). La fréquence du FSB passe à 667 MHz pour la gamme M (9150, 9140 et 9130) contre 400/533 pour le reste de la série.


3.4.10 Les processeurs multi-coeurs

Depuis plusieurs années, les fabricants d'ordinateurs proposent des machines qui possèdent plusieurs processeurs : on qualifie ces machines de bi/quadri ou octo-processeurs suivant qu'elles connectent sur la même carte mère 2, 4 ou 8 processeurs. Il existera donc 2, 4 ou 8 sockets sur la même carte mère.

Le multi-coeur (multicore) consiste à ne disposer que d'un seul socket pour connecter un processeur composé de plusieurs coeurs de processeur. L'intérêt par rapport à la solution précédente réside dans le gain de performance. Les données n'ont pas en effet à transiter entre 2 processeurs en passant sur la carte mère.

Définition : Coeur de processeur
Le coeur comprend les éléments suivants :
  • les caches L1
  • les circuits de décodage des instructions
  • les circuits de prédiction de branchement
  • les unités d'exécution
  • les registres

a) l'hyperthreading

L'hyperthreading (où Simultaneous Multithreading = SMT) représente la première tentative chez Intel de simulation d'un processeur double coeur. Cette technologie a été mise en place sur le Pentium 4 fonctionnant à 3.06 Ghz HT sorti en Novembre 2002.

L'hyperthreading permet d'utiliser au mieux les ressources du processeur lorsqu'il est amené à gérer plusieurs threads simultanément. L'HT apporte un gain de performance minime mais une souplesse d'utilisation indéniable. Notamment lorsqu'un programme nécessite énormément de ressources, l'hyperthreading permet de garder la main sur le système.


b) les processeurs dual-core

Les premiers processeurs double coeur pour PC sont apparus en Avril 2005. Il s'agissait des Pentium D d'Intel et des Athlon 64 X2 d'AMD.

Processeur Fréquence Cache L2 Prix
Athlon 64 X2 4200 + 2200 Mhz 2 x 512 ko $537
Athlon 64 X2 4800 + 2400 Mhz 2 x 1024 ko $1001
Pentium D 820 2800 Mhz 2 x 1024 ko $241
Pentium EE 840 3200 Mhz 2 x 1024 ko $999
Prix et caractéristiques avril 2005


Intel et le dual-core

Les premiers Pentium 4 D (coeur Smithfield) n'ont pas été un succés. En fait Intel a voulu faire un effet d'annonce, en mettant le premier sur le marché, un processeur dual-core de manière à reléguer AMD au second plan. Cependant, le Pentium 4 D Smithfield est un processeur fait "à la va-vite". Il est composé de deux coeurs Prescott et chaque coeur dispose de sa propre interface de bus. Lorsque les coeurs communiquent ils doivent passer par le FSB. Ce système est donc équivalent en quelque sorte à un système bi-processeur.


Pentium 4 à coeur Smithfield

L'évolution du Smithfield est le Presler.

Coeur Gravure Surface Transistors Cache L2 FSB
Smithfield 90 nm 206 mm2 230 Millions 2 x 1 Mo 200 Mhz (x4)
Presler 65 nm 376 mm2 376 Millions 2 x 2 Mo 266 Mhz (x4)

Tableau comparatif Pentium 4 D Smithfield / Presler


AMD et le dual-core

Les processeurs AMD dual-core (X2) ont été pensés depuis plusieurs années. Leur conception est donc bien plus aboutie que celle des Pentium D d'Intel. Les premiers Athlon X2 ont été mis en service le 31 Mai 2005. Cependant, ils ne sont pas plus performants que les Intel Pentium D avec coeur Presler.


Schéma des Athlon 64 X2 d'AMD


Coeur Gravure Surface Transistors Cache L2 FSB
Manchester, Brisbane (AM2) 90 nm 199 mm2 233 Millions 2 x 512 ko 200 Mhz (x2)
Toledo, Windsor (AM2) 90 nm ? mm2 ? Millions 2 x 512 ko 200 Mhz (x2)

Tableau comparatif Athlon 64 X2 Manchester, Brisbane / Toledo, Windsor


c) Intel et le quad-core

Intel annonce lors de l'IDF de Septembre 2006 la venue prochaine du Quadro un processeur quad-core, muni de 4 coeurs donc.

Ce processeur le QX6700 (core Kentsfield) correspond à la mise en place de deux Core 2 Duo mis côte à côte. En gros, il ne s'agit pas d'un véritable quad core, mais Intel nous refait le coup du Pentium D (2 coeurs E6700 à 2,66 Ghz).

Le Kentsfield n'apporte aucun gain par rapport au E6700 sauf pour les applications multithreadées, capables de répartir la tâche totale sur les différents coeurs.

Source TT-Hardware (12 février 2007)

Intel a pour la première fois atteint le cap mytique du Teraflops en 1996 avec presque 10 000 processeurs Pentium Pro 200 MHz interconnectés. L'ASCI Red consommait ainsi plus de 500 kW et autant pour la climatisation des locaux soit 1000 kW... Une dizaine d'années plus tard, Intel atteint à nouveau 1 Teraflops mais avec un seul processeur à 3,16 GHz et avec une consommation de ridicule de 62 Watts !

Présenté lors de l'ISSCC (International Solid State Circuits Conference), le TeraScale se compose de 80 cores comprenant notamment 2 unités de calcul en virgule flottante et des ports de communication. Il ne s'agit pas ici de cores comparables à ceux des processeurs classiques que nous connaissons. Ils utilisent en outre une architecture spécifique VLIW (Very Long Instruction Word). La puce compte un total de 100 millions de transistors gravés en 65 nm et occupe 275 mm. A titre indicatif, un Core 2 Duo compte 290 millions de transistors répartis sur 143 mm². A 3,16 GHz, le TeraScale ne demande qu'une tension de 0,95 v et consomme 62 watts pour dégager une puissance de calcul de 1,01 Teraflops...

Selon Intel, ce type de puce composée de cores interconnectés par un réseau maillé en 2D permet des développements beaucoup plus rapides. La conception d'un processeur traditionnel comparable aurait demandé deux fois plus de temps. Justin Rattner, directeur technique d'Intel, estime que ce type de processeur pourrait arriver dans le commerce d'ici moins de cinq ans.

On peut penser que la nouvelle ligne directrice chez Intel est à la multiplication des cores simples. Un tendance qui se retrouve dans le projet de processeur graphique Intel prévu pour 2009.



d) AMD et le quad-core

AMD a annoncé le 29 Septembre 2006, qu'il allait produire un CPU quad core K8L en 65 nm : le Barcelona (ou Altair). Il disposera de 4 x 64 Ko de cache L1, de 4 x 512 Ko de cache L2 et de 2 Mo de cache L3 partagés.


10 septembre 2007: annonce de la sortie du K10 Barcelona (serveur) qui est une architecture 4 cores :

  • chaque core dispose de 128 Ko (64+64) de cache L1 et 512 Ko de cache L2
  • d'unités SSE de 128 bits
  • il existe un cache L3 unifié de 2 Mo
  • 3 liens HyperTransport 1.0
  • 463 millions de transistors gravés en technologie 65 nm SOI

Novembre 2007 : les premiers tests effectués sur les AMD Phenom 9600 (2,3 Ghz) montrent que ceux-ci sont bien moins performants que les Quad Core Intel Q6600 sortis depuis plus d'un an (TomsHardware.com a noté qu'un Phenom 9600 était en moyenne 13,5% moins performant qu'un Intel Q6600). Etant donné que ces 2 processeurs coûtent dans les 220 €, il n'y a aucun intérêt à acheter un Phenom : coup dur pour AMD !

Décembre 2007 : on apprend que le Phenom souffrirait d'un bug lié au cache L3 qui a pour conséquence de geler le système. Un patch peut être appliqué au BIOS et permet d'éviter le bug, mais on note alors une baisse de 10% en moyenne des performances !!Il faudra attendre la révision B3 prévue pour le début 2008.

Décembre 2007 : on notera qu'Intel a également détecté un bug sur ses Quad Core (Yorkfield, 45nm). Il faudra attendre mars 2008 pour disposer de processeurs exempts de bugs.


c) AMD et le Tri-core

Les deux premiers modèles du genre ont pour nom de code Toliman (T7600 et T7700), ils seront gravés en 65 nm, compatibles HyperTransport 3.0 et équipés d'un contrôleur de mémoire vive DDR2-800 intégré. Le tout viendra s'enficher sur un socket AM2+.


e) Intel et l'hexa-core

Au mois de Septembre 2008, Intel a inauguré la série des processeurs Xeon Dunnington X7400 à 6 coeurs. Ces processeurs sont composés de 3 die à double-coeurs. Le prix du processeur est de $2700 environ.

Processeur Coeurs Fréquence Cache L3 Consommation Prix*
X7460 6 2,66 GHz 16 Mo 130W 2 729
L7455 6 2,13 GHz 12 Mo 65W 2 729
E7450 6 2,40 GHz 12 Mo 90W 2 301
L7445 4 2,13 GHz 12 Mo 50W 1 980
E7440 4 2,40 GHz 16 Mo 90W 1 980
E7430 4 2,13 GHz 12 Mo 90W 1 391
E7420 4 2,13 GHz 8 Mo 90W 1 177

e') AMD et l'hexa-core

Les premiers processeurs hexa-core d'AMD (Istanbul) font leur apparition au Japon fin Juin 2009 : gravés en 45 nm, avec une fréquence de 2.4 GHz, 512 Ko de mémoire cache L2 par core (3072 Ko totaux donc) et 6 Mo de cache L3 partagés (AMD Opteron 2427 à 2.2 GHz, et 2431 à 2.4 GHz).


f) SUN et l'octo-core

Sun a annoncé en 2006 la sortie de son nouveau processeur l'UltraSparc T1 (Niagara) qui dispose de 8 cores.

Chaque core dispose de 4 threads d'exécution, ce qui permet sur un processeur unique d'exécuter 32 tâches simultanées exécutées en parallèle.



Le 7 août 2007, Sun annonce l'arrivée prochaine du T2 :

  • dissipation thermique : 95 à 103 W
  • Coeurs : 8 (8 threads par coeur)
  • Threads : 64

Autres solutions :



g) single vs dual vs quad




3.4.11 Les processeurs 64 bits

AMD fut le premier à introduire la technologie 64 bits en 2003 avec ses processeurs Athlon 64 (architecture K8). Les Athlon 64 ont pour caractéristique de pouvoir exécuter des programmes aussi bien en 32 bits qu'en 64 bits. On parle également d'architecture x86-64 (EMT64 chez Intel, AMD64 chez AMD): elle assure la compatibilité avec les applications 32 bits existantes mais laisse la possibilité de passer au 64 bits.

Un processeur est dit 32 bits ou 64 bits si ses registres généraux ont une taille de 32 ou 64 bits. Un saut technologique avait déjà été franchi lors de l'apparition du Intel 80386 qui permettait le passage du 16 bits au 32 bits.

Quel est l'intérêt de passer au 64 bits ? Les applications qui bénéficieront du passage au 64 bits sont les applications de calcul scientifique principalement.

En mode 64 bits, les 8 registres généraux passent en 64 bits (EAX devient RAX) et 8 nouveaux registres supplémentaires apparaissent (R8 à R15).


Qu'en est-il des performances de l'architecture 64 bits ?

  • si on exécute un programme 32 bits dans une architecture 64 bits, on obtient normalement les mêmes performances qu'en 32 bits, ce qui semble normal.
  • par contre, si on recompile un programme en 64 bits, il s'exécute plus rapidement en mode 64 bits que sa version 32 bits. En effet :
    • d'une part le code est moins long (i.e. comprend moins d'instructions, même si généralement la taille du programme est plus longue)
    • et d'autre part, il est naturellement optimisé par le fait que l'on peut utiliser plus de registres et donc faire moins souvent appel à la mémoire en étant contraint de sauvegarder temporairement des données ou de les recharger (cf. produit de matrices en assembleur).

On pourra lire à ce sujet l'article AMD K8 - Partie 3 : Architecture sur www.x86-secret.com (section Article, Rubrique CPU).

Remarque : des tests récents (Août 2007) montrent que pour certains programmes, le passage en 64 bits permet de diminuer de 2/3 le temps d'exécution par rapport au 32 bits avec les Core 2 Duo (cf PCSTATS).





3.4.12 Le Core Duo 2

Les Pentium 4 arrivent en fin de vie en 2006. Intel prévoyait de les faire évoluer pour atteindre des fréquences proches de 7 à 10 Ghz mais s'est heurté à un problème majeur : la consommation de ses processeurs n'a cessé d'augmenter, l'architecture Netburst (introduite avec le Pentium 4) n'étant pas adaptée à la montée en fréquence au delà de 4 Ghz sans utiliser de système de refroidissement conséquent.

Cependant, Intel dispose d'une architecture performante et qui consomme peu d'énergie : l'architecture issue du Pentium Pro et améliorée sur les Pentium II/III puis sur les Pentium M. Intel a donc réalisé la synthèse des 2 architectures Pentium Pro et Netburst pour donner naissance à l'architecture Core Duo en 2006 avec le Yonah.

Architecture : P5 P6 Netburst Banias
Représentant : Pentium (1993) Pentium Pro (1995)
Pentium II (1997)
Pentium III (1998)
Pentium 4 (2000)
Pentium D (2005)
Xeon
Pentium M
Core Duo (2006)

L'intérêt pour Intel est de pouvoir disposer d'une seule architecture pour 3 plateformes : desktop, mobile et server.

Architecture Conroe Merom Woodcrest
Plateforme desktop mobile server
finesse de gravure : 65nm
Wide Dynamic Execution
Advanced Digital Media Boost
Advanced Smart Cache
Smart Memory Access and Intelligent Power Capability

Intel Core Duo 2

Pour de plus amples précisions sur l'architecture Core 2 Duo :


3.4.13.a Processeurs les plus puissants (Septembre 2006)

En septembre 2006, les processeurs les plus performants sont :

  • Les Core 2 Duo : X6800, E6700, E6600
  • Les Athlon 64 FX 62 et 60
  • L'Athlon 64 X2 4800


3.4.13.b Processeurs les plus puissants (Novembre 2007)

En novembre 2007, les processeurs les plus performants sont :

  • Le Core 2 Quad Extreme (Penryn) QX9650 : 3,0 ghz, 2x6Mo de cache L2, FSB 1333 Mhz, prix $999
  • Le Core 2 Quad (Penryn) Q9550 2,83 ghz, 2x6Mo de cache L2, FSB 1333 Mhz, prix $530
  • AMD Athlon 6000 et 6400
  • AMD Athlon Phenom



3.4.14 Quelques spécifications des processeurs Intel


Processeur Apparition Fréquence (initiale) Transistors Registres Bus de données Espace d'adressage Cache

8086

1978

8 MHz

29 K

16-bit general purpose registers (GP)

16

1 MBytes

-

80286

1982

12,5 MHz

134 K

16-bit GP

16

16 MBytes

-

80386DX

1985

20 MHz

275 K

32-bit GP 

32

4 GBytes

-

80486DX

1989

25 MHz

1,2 M

32-bit GP, 80-bit FPU

32

4 GBytes

8 KBytes
L1

Pentium

1993

60 MHz

3,1 M

32-bit GP, 80-bit FPU

64

4 GBytes

16 KBytes
L1

Pentium Pro

1995

150 MHz

5,5 M

32-bit GP, 80-bit FPU

64

64 GBytes

16 KBytes L1; 256, 512, 1 K L2

Pentium II

1997

266 MHz

7 M

32-bit GP, 80-bit FPU, 64-bit MMX

64

64 GBytes

32 KBytes L1; 256, 512 KBytes L2

Pentium III

1999

500 MHz

8,2 M

32-bit GP, 80-bit FPU, 64-bit MMX, 128-bit XMM

64

64 GBytes

32 KBytes L1; 256, 512 KBytes L2

Voici quelques spécifications plus poussées des processeurs Intel et AMD tirées du site TomsHardware :


3.5 Gamme de processeurs

Il semble aujourd'hui bien difficile de présenter brièvement la gamme des processeurs disponibles. Les critères de différenciation sont nombreux et peuvent être combinés :

  • architecture
  • fréquence de fonctionnement du processeur
  • fréquence du FSB
  • présence ou non de l'HyperThreading
  • technologie 64 bits
  • dual-core ou non
voir le comparateur de processeurs Intel.


entrée de gamme machine dotée d'un petit cache L2 et d'un FSB pas très véloce
milieu de gamme machine dotée d'un cache L2 de taille moyenne et d'un FSB rapide
haut de gamme machine dotée d'un cache L2 de grande taille (voire d'un cache L3) et d'un FSB rapide
gamme mobile machine dotée de fonctionnalités en rapport avec la gestion de l'énergie
gamme ultra mobile machine dotée de fonctionnalités en rapport avec la gestion très fine de l'énergie


Gamme Intel AMD
Architecture 32 / 64 bits
bas de gamme
milieu de gamme
haut de gamme
gamme mobile
gamme ultra mobile
Architecture 128 bits
haut de gamme  


Processeur Taille du die
(mm2)
# Transistors
(millions)
Finesse
(nm)
Core 2 Extreme X6800 143 291 65
Core 2 Duo E6600 143 291 65
Core 2 Duo E6400 111 167 65
Pentium D 900 280 376 65
Athlon 64 FX-62 230 227 90
Athlon 64 5000+ 183 154 90
Atom 230 25 47 45
Atom 330 50 94 45
Core 2 Extreme QX6700 ? 582 45
Caractéristiques

3.6 Assembleur et optimisation de code

3.6.0 introduction

L'optimisation du code est un art, mais celle-ci ne doit pas intervenir lors de la création d'un programme mais une fois que le programme fonctionne correctement. Il faut en outre vérifier que le programme libère bien la mémoire allouée et qu'il n'existe pas de fuites mémoire (memory leaks) qui correspondent à des oublis de libération de mémoire précédemment allouée.

Pour optimiser un programme il faut tout d'abord déterminer quelles parties consomment le plus de temps CPU. C'est ce que l'on appelle le profilage (profiling) du programme.

Compiler le programmme à optimiser avec les options -pg. Exemple avec le programme suivant qui calcule la suite de fibonacci en utilisant une fonction récursive et une fonction itérative (fib.c) :

 
gcc -pg -o fib.exe fib.c

./fib.exe

gprof fib.exe >fib_result.txt

Le fichier résultat fib_result.txt montre que la fonction récursive prend plus de temps que la fonction itérative :

  • fib_recurs : 8.13s (self seconds), appelée 331.160.280 fois
  • fib_iterat : 0.14s (self seconds), appelée 1 fois

Selon Intel, les facteurs les plus importants pour optimiser les performances d'un cpu sont les suivants :

  • bonne prédiction de branchement
  • éviter les accès mémoires qui provoquent des attentes
  • obtenir de bonnes performances dans le calcul des nombres réels
  • vectorisation

3.6.1 instruction assembleur sans équivalent

Voici un exemple ou une instruction assembleur n'a pas d'équivalent dans un langage comme le C. L'instruction bsr (voir aussi bsf) calcule la position du bit le plus significatif de son opérande.

On peut programmer cette fonction en C :

long i;
for (i = (number >> 1), position = 0; i != 0; ++position) i >>= 1;

Une autre solution consiste à inclure du code assembleur à l'intérieur d'un morceau de code en C :

asm ("bsrl %1, %0" : "=r" (position) : "r" (number));
La solution n'est pas la meilleure en terme de portabilité, mais en C on peut contourner la difficulté en utilisant les directives de compilation en fonction de l'architecture.

  Code en C Code asm
Temp d'exécution (109) 28 s 2,88 s
Temps d'exécution pour 109 itérations

3.6.2 utilisation des sous-programmes de base

Il est préférable d'utiliser les sous-programmes de base comme memmove pour déplacer des zones de mémoire ou memcpy pour recopier une zone de mémoire :

Remplacer :
char *dst, *src;

for (i = 0; i < N; ++i) {
  dst[i] = src[i]
}
par
memcpy(dst,src,N);

3.6.3 amélioration de la prédiction de branchement
Exemple 1

Les instructions de la forme :

X = (A < B) ? CONST1 : CONST2;

sont généralement traduites en :
  cmp A,B
  jge L30
  mov ebx,CONST1
  jmp L31
L30:
  mov ebx,CONST2
L31:

Pour en améliorer l'efficacité il faut supprimer les différents sauts :

xor ebx,ebx
cmp A,B
setge bl
sub ebx,1
and ebx,CONST3 ; CONST3 = CONST1-CONST2
add ebx,CONST2

On utilise ici l'instruction setcc (set byte on condition) :

global main

section .data

 A: dd 2
 B: dd 1

CONST1 EQU 10
CONST2 EQU 30
CONST3 EQU -20

section .text

main:
  xor   ebx,ebx
  mov   eax,[A]
  cmp   eax,[B]
  setge bl
  sub   ebx,1
  and   ebx,CONST3 ; CONST3 = CONST1-CONST2
  add   ebx,CONST2

Exemple 2

Autre exemple qui correspond à l'instruction :

if (X == 0) A = B;

La traduction classique de cette instruction est la suivante, elle utilise un saut conditionnel :

  test ecx,ecx
  jne L10
  mov eax,ebx
L10:

Il faut la remplacer par les instructions suivantes qui utilisent l'instruction CMOVcc (Conditional MOVe) :

test  ecx,ecx
cmove eax,ebx



3.6.4 optimisation des boucles (dépliage et blocage)
dépliage de boucle

Pour tirer le maximum des unités vectorielles, il faut éviter d'écrire :

double a[100], sum;
int i;
sum = 0.0f;
for (i = 0; i < 100; i++) {
     sum += a[i];
}
et déplier les boucles :
double a[100], sum1, sum2, sum3, sum4, sum;
int i;
sum1 = 0.0;
sum2 = 0.0;
sum3 = 0.0;
sum4 = 0.0;
for (i = 0; i < 100; i + 4) {
    sum1 += a[i];
    sum2 += a[i+1];
    sum3 += a[i+2];
    sum4 += a[i+3];
}
sum = (sum4 + sum3) + (sum1 + sum2);

Autre exemple tiré du Software Optimization Guide for AMD64 Processors. Sur l'exemple suivant, il vaut mieux réaliser un dépliage de 2 :

double a[MAX], b[MAX];

for (int i=0; i < MAX; ++i) {
  a[i] = a[i] + b[i];
}

// a remplacer par 

for (int i=0; i < MAX; i+=2) {
  a[i] = a[i] + b[i];
  a[i+1] = a[i+1] + b[i+1];
}

Le premier exemple est traduit par une boucle comportant 7 instructions. Sachant que les Athlon64 sont capables de décoder 3 instructions par cycle d'horloge et que l'unité arithmétique sur les réels n'est capable de réaliser qu'une addition par cycle, on en déduit que l'efficacité de cette boucle est de 3/7 addition par cycle soit 0,429 add/s.

  mov ecx,MAX
  mov eax,a
  mov ebx,b
add_loop:
  fld  QWORD [eax]
  fadd QWORD [ebx]
  fstp QWORD [eax]
  add  eax,8
  add  ebx,8
  dec  ecx
  jnz  add_loop

En dépliant la boucle, on utilise 10 instructions et on réalise 2 additions par itération de la boucle, cela donne alors une efficacité de 6/10 addition par cycle, soit 0,6 add/s
  shr  ecx,1
add_loop:
  fld  QWORD [eax]
  fadd QWORD [ebx]
  fstp QWORD [eax]
  fld  QWORD [eax+8]
  fadd QWORD [ebx+8]
  fstp QWORD [eax+8]
  add  eax,16
  add  ebx,16
  dec  ecx
  jnz  add_loop

On réalisant ce programme en C, on obtient les résultats suivants :

Dépliage Langage Temps Options de compilation
1 C 1m38s  
2 C 1m37s  
4 C 1m37s  
1 C 1m34s -O2
2 C 1m34s -O2
4 C 1m32s -O2
1 asm 1m35s  
2 asm 1m35s  
4 asm 1m41s  
Exécution 100.000 fois sur des tableaux de 100.000 double
sur un pentium 4 prescott 2,8 Ghz


blocage (loop blocking)

Le blocage de boucle est une technique d'optimisation des performances de la mémoire. L'objectif du blocage de boucle consiste à éliminer le plus possible de défauts de cache. On transforme ici le problème initial en plusieurs sous-problèmes de manière à ne travailler que sur une partie de la mémoire :

Prenons l'exemple suivant :

float A[MAX][MAX], B[MAX][MAX];

for (i=0; i < MAX; i++) {
  for (j=0; j < MAX; j++) {
         A[i][j] = A[i][j] + B[j][i];
     }
}

Le code produit n'est pas efficace car on obtient énormément de défaut de cache. Il vaut mieux le réécrire sous la forme suivante :

float A[MAX][MAX], B[MAX][MAX];

for (i=0; i < MAX; i+=block_size) {
     for (j=0; j < MAX; j+=block_size) {
         for (ii=i; ii < i+block_size; ii++) {
              for (jj=j; jj < j+block_size; jj++) {
                  A[ii][jj] = A[ii][jj] + B[jj][ii];
              }
         }
     }
}


3.6.5 vectorisation
Exemple 1

Supprimer les dépendances de données permet au coeur qui exécute les instructions dans le désordre d'extraire plus de parallélisme des instructions. Par exemple, pour calculer z = a + b + c + d, plutôt que d'écrire :

   x = a + b;
   y = x + c;
   z = y + d;
il est préférable d'écrire :
   x = a + b;
   y = c + d;
   z = x + y;


Exemple 2

On peut également tirer partie des unités vectorielles en utilisant des instructions MMX et SSE. Au lieu d'écrire :

void add(float *a, float *b, float *c)
{
  int i;
  for (i = 0; i < 4; i++) {
    c[i] = a[i] + b[i];
  }
}

préférer le code suivant :

void add(float *a, float *b, float *c)
{
  __asm {
    mov     eax, a
    mov     edx, b
    mov     ecx, c
    movaps  xmm0, XMMWORD PTR [eax]
    addps   xmm0, XMMWORD PTR [edx]
    movaps  XMMWORD PTR [ecx], xmm0
  }
}
où l'instruction movaps (move aligned packed single precision floating point values - 128 bits) permet d'améliorer le rendement.

Exemple 3 - and

On désire améliorer le traitement de base suivant qui consiste à réaliser un and entre deux vecteurs de caractères :
void function(char *vA, char *vB, char *vC, int len) {
  int i;

  for (i = 0; i < len; i++) {
    vC[i] = vA[i] & vB[i];
  }
}
Amélioration Pentium-M Pentium III Pentium 4 Pentium D C2D E6420 Athlon 64 Sempron
-O2 27.450s 276s 38.296 24.794 17.917s 28.186 36.549
dépliage 4 21.561s - 34.956 21.313 18.065 25.110 33.130
dépliage 8 21.413s - 35.123 20.749 17.877 24.166 32.302
dépliage 16 21.221s - 35.068 20.969 17.881 24.338 32.310
traite par 4 (eax) 16.881s - 33.043 17.969 5.380 24.718 32.466
traite par 4 (eax)
+ dépliage 4
6.624s 185.0 30.713 14.605 5.244 19.597 28.974
vectorisation (sse2) 5.788s
(-80%)
179.0
(-33%)
29.125
(-22%)
15.085
(-39%)
4.004
(-77%)
20.157
(-28%)
29.846
(-19%)
vectorisation (sse2)
+ unroll 4
6.044s 182.0 28.757 14.997 4.028 20.329 29.710

Amélioration E8400 Q6600 Q9300 P9500 Core i7 860
-O2 12.62s 15.84s 15.77s 18.15s 13.38s
dépliage 4 12.65s 16.06s 15.88s 10.09s 7.27s
dépliage 8 12.64s 15.73s 15.29s 14.50s 10.11s
dépliage 16 12.40s 15.91s 15.20s 14.10s 10.12s
traite par 4 (eax) 7.92s 4.82s 4.56s 6.48s 5.34s
traite par 4 (eax)
+ dépliage 4
3.88s 4.58s 4.66s 8.99s 10.37s
vectorisation (sse2) 2.98s
(-77%)
3.64s
(-77%)
3.54s
(-77%)
3.22s
(-82%)
2.00s
(-85%)
vectorisation (sse2)
+ unroll 4
3.00s 3.68s 3.47s 3.22s 1.92s
Temps en secondes pour 20.000 itérations

  • Pentium III Tualatin, 1.4 Ghz, FSB 100/133 Mhz, 256 Mo SDRAM PC 100
  • Pentium-M, 2.0 Ghz, FSB 533 Mhz, 1 Go DDR-SDRAM
  • Pentium 4 Northwood, 2.8 Ghz, FSB 800 Mhz, 256 Mo DDR-SDRAM PC3200
  • Pentium D 830 Smithfield, 3 Ghz, FSB 800 Mhz, 1 Go DDR2-SDRAM PC4300
  • Core 2 Duo E6420, 2.13 Ghz, FSB 1066 Mhz, 2 Go DDR2-SDRAM PC 6400
  • Core 2 Duo E8400, 3.00 Ghz, FSB 1333 Mhz, 3 Go DDR2-SDRAM PC 6400
  • Core 2 Quad Q6600, 2.40 Ghz, FSB 1066 Mhz, 3 Go DDR2-SDRAM PC 5300
  • Core 2 Quad Q9300, 2.50 Ghz, FSB 1333 Mhz, 3 Go DDR2-SDRAM PC 6400
  • Athlon 64 X2 4200+, 4.2 Ghz (2.2 Ghz), FSB 400 Mhz, 512 Mo DDR-SDRAM PC 3200
  • Sempron 3000+, FSB 400 Mhz, 512 Mo DDR-SDRAM PC 3200
  • Intel Core 2 Duo P9500, 2.53 Ghz, FSB 800 Mhz, 4 Go DDR2-SDRAM
  • Intel Core i7 860, 2.8 Ghz, 4 Go DDR3-SDRAM PC1333

sources disponibles.

Ces résultats nous amènent à faire les remarques suivantes :

  • le dépliage a une faible influence et permet de gagner quelques % excepté sur le Core 2 Duo (!)
  • le traitement par groupe de 4 octets (et non un par un) peut avoir une influence importante
  • l'utilisation des instructions SSE est intéressante et apporte un gain égal au traitement par 4 + dépliage


Exemple 4 - parcimonie

On désire améliorer le traitement de base suivant qui consiste à calculer le score de parcimonie de deux séquences d'acides aminés.
int parsimony(char *vA, char *vB, char *vC, int len) {
  int i, f=0;

  for (i = 0; i < len; i++) {
    vC[i] = vA[i] & vB[i];
    if (vC[i] == 0) {
      vC[i] = vA[i] | vB[i];
      ++f;
    }
  }
  return f;
}
Amélioration Pentium-M Pentium III Pentium 4 Pentium D C2D E6420 Athlon 64 Sempron
-O2 48.287s - 60.153 63.000 46.415 43.879 53.723
dépliage 4 47.415
(-1.8%)
- 52.378 56.098
(-11%)
44.959
(-3%)
39.538
(-10%)
48.675
traite par 4 44.143s
(-8.5%)
- 48.668
(-19%)
55.027
(-12%)
40.943
(-12%)
37.726
(-14%)
46.275
vectorisation 1 (sse) 14.265 - 38.286 40.012 9.025 11.953 16.409
vectorisation 2 (sse) 7.381s
(-84%)
- 15.612
(-74%)
8.825
(-85%)
2.852
(-93%)
11.869
(-72%)
16.033
(-70%)
vectorisation 3 (sse) 7.896s - 15.531 9.209 3.568 11.981 16.317
vectorisation 4 (sse) 7.704s - 15.730 9.269 2.998 11.753 16.033

Amélioration C2D E8400 C2Q Q6600 C2Q Q9300 C2Q P9500 i7 860
-O2 29.22/43.80 40.91 39.35 37.76 30.58
dépliage 4 28.36/42.45
(-3%)
39.69 38.66 42.92 38.67
traite par 4 31.22 36.50 36.17 33.71 28.39
vectorisation 1 (sse) 8.92 8.36 8.02 7.60 6.06
vectorisation 2 (sse) 2.61
(-91%)
2.47
(-93%)
2.36
(-94%)
2.23
(-94%)
1.89
(-93%)
vectorisation 3 (sse) 3.33 3.12 2.99 2.85 2.20
vectorisation 4 (sse) 1.72 2.35 2.31 2.14 1.84
vectorisation 2 (sse+POPCNT) - - - - 1.49
Temps en secondes pour 10.000 itérations (Linux Ubuntu)

  • sources disponibles / available.
  • pour plus de détail / for more details >>

Ces résultats nous amènent à faire les remarques suivantes :

  • le dépliage ou le traitement par groupe de 4 octets (et non un par un) peuvent permettre de gagner de 10 à 20 %
  • la vectorisation (méthode 1, utilisation de BT) est beaucoup moins efficace que les autres versions (table de conversion), sauf pour les AMD pour lesquels on obtient les mêmes résultats
  • la vectorisation apporte un gain de 70 à 90 %

Exemple 5 - strlen

Amélioration de la fonction strlen qui calcule la longueur d'une chaîne de caractères en langage C.

Longueur strlen strlen_c strlen_sse1 strlen_sse2 strlen_sse3 strlen_scasb
10 0.11 0.21 0.10 0.10 0.08 0.58
20 0.13 0.20 0.34 0.36 0.12 0.85
50 0.23 0.47 0.19 0.22 0.21 0.66
200 0.70 1.40 0.55 0.61 0.68 6.00
500 1.70 3.24 1.24 1.38 1.76 13.98
960 3.14 6.02 2.27 2.51 3.28 27.02
Temps en secondes Core 2 Duo E6420 2.13 Ghz (Linux Ubuntu)


Amélioration Pentium-M Pentium III Pentium 4 Pentium D C2D E6420 Athlon 64 Sempron
strlen (std) 4.936 - 4.454 4.264 3.144 4.884 6.284
strlen (perso) 9.529 - 7.475 7.752 5.820 9.077 11.745
vectorisation (1) eax 2.732 - 3.981 2.352 2.244 4.320 5.420
vectorisation (2) ecx 3.052 - 3.981 2.284 2.528 3.892 4.968
vectorisation (3) bsf 3.844 - 7.668 9.989 3.252 7.124 9.437
repnz scasb 18.793 - 14.358 12.953 26.410 9.553 12.149
Temps en secondes pour 10.000 itérations (Linux Ubuntu)
pour chaînes de 960 caractères

  • la fonction strlen de la librairie standard C est optimisée
  • le temps de traitement de l'instruction bsf a été amélioré sur Pentium-M et Core 2 Duo
  • la vectorisation + utilisation d'une table de conversion + eax se révèle la plus intéressante sauf pour les processeurs AMD
  • sur Athlon 64 X2, la commande rep scasb est estimée prendre 15 + (5/2 × ECX) cycles horloge


Exemple 6 - align

Projet de programmation 2009-2010 : vectorisation de l'algorithme de programmation dynamique de Needleman et Wunsch.

Méthode/CPU Pentium D Pentium-M Q6600 Q9300 i7 860
method 1 (C code) 56.67 21.79 18.98 17.74 13.28
method 2 (asm vect unaligned) 114.05 47.76 36.25 30.74 25.90
method 3 (asm vect aligned) 51.37 51.31 26.45 22.19 20.15
method 4 (asm vect aligned version 2) 27.26 38.43 18.76 15.46 12.45
method 5 (asm vect aligned version 3) 39.66 46.61 26.24 20.54 19.78
method 6 (asm vect aligned version 4) 23.90 39.04 17.55 14.60 10.66
method 7 (asm vect aligned version 5) - - - 11.42 7.73
gain (method 1/method 7) - - - 35 % 41 %
Temps en secondes pour 50.000 exécutions de l'algorithme
avec deux séquences de 195 résidus (Linux Ubuntu)

Description des algorithmes :

  • method 1 : code C du sujet
  • method 2 : traduction de method 1 avec données non alignées
  • method 3 : traduction de method 1 avec données alignées
  • method 4 : amélioration de method 3
  • method 6 : amélioration de method 3
  • method 7 : amélioration de method 3 avec utilisation d'instructions SSE4.1

Machines utilisées :

  • Pentium-M, 2.0 Ghz, FSB 533 Mhz, 1 Go DDR-SDRAM
  • Pentium D 830 Smithfield, 3 Ghz, FSB 800 Mhz, 1 Go DDR2-SDRAM PC4300
  • Core 2 Quad Q6600, 2.40 Ghz, FSB 1066 Mhz, 3 Go DDR2-SDRAM PC 5300
  • Core 2 Quad Q9300, 2.50 Ghz, FSB 1333 Mhz, 3 Go DDR2-SDRAM PC 6400
  • Intel Core i7 860 @ 2.8 Ghz
3.6.6 options de compilation

Le compilateur gcc possède un certain nombre d'options qui permettent d'améliorer l'optimisation du code :

  • -O2 optimisations de niveau 2
  • -O3 optimisations de niveau 3 (inclu le niveau 2)
  • -falign-functions=n où n est une puissance de 2, alignement du début du code des fonctions
  • -falign-loops=n de même pour les boucles
  • -floop-optimize et -floop-optimize2 : permet de déplacer les constantes hors des boucles, simplification du test de sortie
  • -funroll-loops dépliage des boucles uniquement si le nombre d'itérations est connu lors de la compilation
  • -funroll-all-loops dépliage des boucles même si le nombre d'itérations n'est pas connu lors de la compilation

Pour chaque architecture, on dispose également d'un certain nombre d'options d'optimisation :

  • -mtune=i386,i586,i686,pentium2,pentium3,pentium4,pentium-m,athlon,athlon64 génère du code pour l'architecture spécifiée
  • -march=.. sur le même modèle que -mtune
  • -mfpmath=387,sse calcul sur les réels avec coprocesseur ou unités SSE
  • -mmmx, -msse, -msse2, -msse3, -m3dnow
  • -m32, -m64 génère du code pour une architecture 32 ou 64 bits
  • ensemble d'options qui semblent donner de bons résultats : -O3 -fno-thread-jumps -fno-guess-branch-probability -fno-cprop-registers -fno-if-conversion -fno-delayed-branch -foptimize-sibling-calls -fcse-follow-jumps -fgcse -fexpensive-optimizations -fstrength-reduce -frerun-cse-after-loop -frerun-loop-opt -fcaller-saves -fpeephole2 -fregmove -fstrict-aliasing -freorder-blocks -fsched-spec -freorder-functions -falign-loops -funit-at-a-time -fprefetch-loop-arrays -fno-inline -fpeel-loops -ftracer -funswitch-loops -fbranch-target-load-optimize -mno-push-args -mno-align-stringops -mfpmath=387 -fno-math-errno -fno-trapping-math -ffinite-math-only, réaliser l'édition de liens éventuellement avec -lrt -lm
voir la documentation de gcc (version 4.2.1) pour plus de renseignements ou consulter le livre : The Definitive Guide to GCC de William Von Hagen, Apress, 2006, ISBN 1-59059-585-8.


Mesure des performances des machines

Mesurer les performances des machines consiste à mesurer les performance du trio chipset + processeur + mémoire. D'autres tests permettent également de tester les performances de la carte graphique (élément essentiel pour les jeux 3D) ainsi que celles du disque dur.

Voici une liste de logiciels qui permettent d'évaluer les performances des machines (sous Windows XP) :

Voici quelques exemples de résultats de ces différents programmes :

News

www.ginjfo.com 02/05/2008 - le memristor

Des chercheurs de chez HP ont découvert un nouveau type de circuit électrique, et ils veulent le développer commercialement. Des scientifiques travaillant à Hewlett-Packard ont annoncé qu'ils ont découvert un quatrième type de base de circuit électrique - tel qu'il permettrait de créer un ordinateur qui "se souvient" de l'état où il était et qui n'a pas besoin d'initialisation.

La théorie de l'électronique identifie trois éléments fondamentaux d'un circuit passif : résistances, condensateurs et inducteurs. Leon Chua, un scientifique à U.C. Berkeley dans les années 70, a démontré (dès 1971, en fait) qu'il devrait également y avoir un quatrième élément fondamental, connu sous le nom de résistance de mémoire, ou « mémristor », et a découvert les équations mathématiques prouvant son existence.

Une équipe de scientifiques à HP a maintenant mis au point un modèle pratique, et non plus simplement théorique comme l'avait fait avant eux l'universitaire, manifestant la faisabilité industrielle de la « mémristance ».

L'équipe, menée par Stanley Williams, met en avant l'idée que les propriétés de la mémristance sont très différentes de n'importe quel autre dispositif électrique. Williams et son équipe ont développé un modèle mathématique et un exemple physique d'un mémristor, qu'ils décrivent dans la publication de référence Nature. Monsieur Williams a comparé les propriétés du mémristor à l'eau traversant un tuyau de jardin. Dans un circuit normal, les écoulements d'eau/courant vont dans plus d'une direction. Mais dans une résistance de mémoire, le tuyau « se rappelle » la direction vers laquelle l'eau/courant coule, et il augmente le débit dans cette direction pour améliorer l'écoulement. De même, si l'eau ou le courant vient de la direction opposée, alors le tuyau/courant se rétrécit dans cette direction. Selon M. Williams, « la mémristance se rappelle la direction et la quantité de charge qui la traverse c'est cela, la mémoire de résistance. C'est très différent de tout autre dispositif électrique, dit Williams, aucune combinaison de résistance, de condensateur ou d'inducteur ne vous donnera cette propriété »

Presence-PC 28/09/2006

Intel prévoit l'utilisation d'une finesse de gravure de 45nm pour graver ses circuits intégrés dès la mi-2007.

De son côté, IBM pense mettre au point le procédé de gravure 32nm fin 2009 ou début 2010. IBM allié à Chartered Semiconductor, Samsung et Infineon a déjà mis au point un procédé de gravure 45nm qui s'annonce 30% plus rapide que le 65nm.


TT-Hardware 16/10/2006

Intel prévoit la sortie du Core 2 Extreme QX6700 le 14 Novembre 2006. Basé sur le Kentsfield, il sera le premier processeur Quad Core commercialisé pour le grand public. Le prix est estimé à $1000.


Précédent Sommaire Suivant

marqueur eStat\'Perso